diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..82c6ef093e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Report something that isn't working. +title: '' +assignees: '' +labels: bug +--- + + + +- Operating System (Linux/Mac/Windows/iOS/Android): +- Core Version: +- Client Version: + +## Expected behavior + +*What did you try to achieve?* + +## Actual behavior + +*What happened instead?* + +### Steps to reproduce the problem + +1. +2. + +### Screenshots + +### Logs + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a3b21399f..e14aa147f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,21 +16,26 @@ on: branches: - main +permissions: {} + env: RUSTFLAGS: -Dwarnings + RUST_VERSION: 1.87.0 + + # Minimum Supported Rust Version + MSRV: 1.85.0 jobs: lint_rust: name: Lint Rust runs-on: ubuntu-latest - env: - RUSTUP_TOOLCHAIN: 1.84.0 steps: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Install rustfmt and clippy - run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy + run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy - name: Cache rust cargo artifacts uses: swatinem/rust-cache@v2 - name: Run rustfmt @@ -42,18 +47,6 @@ jobs: - name: Check with only default features run: cargo check --all-targets - npm_constants: - name: Check if node constants are up to date - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - name: Rebuild constants - run: npm run build:core:constants - - name: Check that constants are not changed - run: git diff --exit-code - cargo_deny: name: cargo deny runs-on: ubuntu-latest @@ -61,6 +54,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@v2 with: arguments: --all-features --workspace @@ -74,6 +68,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Check provider database run: scripts/update-provider-database.sh @@ -86,6 +81,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Cache rust cargo artifacts uses: swatinem/rust-cache@v2 - name: Rustdoc @@ -97,24 +93,36 @@ jobs: matrix: include: - os: ubuntu-latest - rust: 1.84.0 + rust: latest - os: windows-latest - rust: 1.84.0 + rust: latest - os: macos-latest - rust: 1.84.0 + rust: latest - # Minimum Supported Rust Version = 1.81.0 + # Minimum Supported Rust Version - os: ubuntu-latest - rust: 1.81.0 + rust: minimum runs-on: ${{ matrix.os }} steps: + - run: + echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV + shell: bash + if: matrix.rust == 'minimum' + - run: + echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV + shell: bash + if: matrix.rust == 'latest' + - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Install Rust ${{ matrix.rust }} - run: rustup toolchain install --profile minimal ${{ matrix.rust }} - - run: rustup override set ${{ matrix.rust }} + run: rustup toolchain install --profile minimal $RUSTUP_TOOLCHAIN + shell: bash + - run: rustup override set $RUSTUP_TOOLCHAIN + shell: bash - name: Cache rust cargo artifacts uses: swatinem/rust-cache@v2 @@ -147,6 +155,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Cache rust cargo artifacts uses: swatinem/rust-cache@v2 @@ -171,6 +180,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Cache rust cargo artifacts uses: swatinem/rust-cache@v2 @@ -192,6 +202,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Install tox run: pip install tox @@ -234,6 +245,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Download libdeltachat.a uses: actions/download-artifact@v4 @@ -286,6 +298,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Install python uses: actions/setup-python@v5 diff --git a/.github/workflows/deltachat-rpc-server.yml b/.github/workflows/deltachat-rpc-server.yml index 4ed769b62a..1076c75085 100644 --- a/.github/workflows/deltachat-rpc-server.yml +++ b/.github/workflows/deltachat-rpc-server.yml @@ -17,6 +17,8 @@ on: release: types: [published] +permissions: {} + jobs: # Build a version statically linked against musl libc # to avoid problems with glibc version incompatibility. @@ -31,8 +33,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux @@ -55,8 +57,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }} @@ -80,6 +82,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Setup rust target run: rustup target add ${{ matrix.arch }}-apple-darwin @@ -105,8 +108,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build deltachat-rpc-server binaries run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android @@ -132,8 +135,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Download Linux aarch64 binary uses: actions/download-artifact@v4 @@ -245,6 +248,10 @@ jobs: cp result/*.whl dist/ nix build .#deltachat-rpc-server-win32-wheel cp result/*.whl dist/ + nix build .#deltachat-rpc-server-arm64-v8a-android-wheel + cp result/*.whl dist/ + nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel + cp result/*.whl dist/ nix build .#deltachat-rpc-server-source cp result/*.tar.gz dist/ python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos @@ -258,8 +265,9 @@ jobs: if: github.event_name == 'release' env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + REF_NAME: ${{ github.ref_name }} run: | - gh release upload ${{ github.ref_name }} \ + gh release upload "$REF_NAME" \ --repo ${{ github.repository }} \ bin/* dist/* @@ -280,6 +288,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -385,8 +394,9 @@ jobs: if: github.event_name == 'release' env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + REF_NAME: ${{ github.ref_name }} run: | - gh release upload ${{ github.ref_name }} \ + gh release upload "$REF_NAME" \ --repo ${{ github.repository }} \ deltachat-rpc-server/npm-package/*.tgz diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 46e16f869b..c626894fa7 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.3.0 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve a PR diff --git a/.github/workflows/jsonrpc-client-npm-package.yml b/.github/workflows/jsonrpc-client-npm-package.yml index 9d53ae90e4..1e037e2054 100644 --- a/.github/workflows/jsonrpc-client-npm-package.yml +++ b/.github/workflows/jsonrpc-client-npm-package.yml @@ -4,10 +4,12 @@ on: release: types: [published] +permissions: {} + jobs: pack-module: name: "Publish @deltachat/jsonrpc-client" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest permissions: id-token: write contents: read @@ -15,6 +17,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index 0f5df0a2fa..3ead8551c0 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -6,6 +6,8 @@ on: pull_request: branches: [main] +permissions: {} + env: CARGO_TERM_COLOR: always RUST_MIN_STACK: "8388608" @@ -17,6 +19,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Use Node.js 18.x uses: actions/setup-node@v4 with: @@ -34,9 +37,6 @@ jobs: run: npm run test env: CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }} - - name: make sure websocket server version still builds - working-directory: deltachat-jsonrpc - run: cargo build --bin deltachat-jsonrpc-server --features webserver - name: Run linter working-directory: deltachat-jsonrpc/typescript run: npm run prettier:check diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 72a84833cc..522a0e1dd9 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -12,6 +12,8 @@ on: branches: - main +permissions: {} + jobs: format: name: check flake formatting @@ -20,8 +22,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - run: nix fmt # Check that formatting does not change anything. @@ -53,7 +55,9 @@ jobs: - deltachat-rpc-server-aarch64-linux - deltachat-rpc-server-aarch64-linux-wheel - deltachat-rpc-server-arm64-v8a-android + - deltachat-rpc-server-arm64-v8a-android-wheel - deltachat-rpc-server-armeabi-v7a-android + - deltachat-rpc-server-armeabi-v7a-android-wheel - deltachat-rpc-server-armv6l-linux - deltachat-rpc-server-armv6l-linux-wheel - deltachat-rpc-server-armv7l-linux @@ -80,8 +84,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - run: nix build .#${{ matrix.installable }} build-macos: @@ -91,14 +95,15 @@ jobs: fail-fast: false matrix: installable: - - deltachat-rpc-server-aarch64-darwin + - deltachat-rpc-server # Fails to bulid + # - deltachat-rpc-server-aarch64-darwin # - deltachat-rpc-server-x86_64-darwin steps: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - run: nix build .#${{ matrix.installable }} diff --git a/.github/workflows/node-docs.yml b/.github/workflows/node-docs.yml deleted file mode 100644 index 8dd777d988..0000000000 --- a/.github/workflows/node-docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -# GitHub Actions workflow to build -# Node.js bindings documentation -# and upload it to the web server. -# Built documentation is available at - -name: Generate & upload node.js documentation - -on: - push: - branches: - - main - -jobs: - generate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - name: npm install and generate documentation - working-directory: node - run: | - npm i --ignore-scripts - npx typedoc - mv docs js - - - name: Upload - uses: horochx/deploy-via-scp@1.1.0 - with: - user: ${{ secrets.USERNAME }} - key: ${{ secrets.KEY }} - host: "delta.chat" - port: 22 - local: "node/js" - remote: "/var/www/html/" diff --git a/.github/workflows/node-package.yml b/.github/workflows/node-package.yml deleted file mode 100644 index bd590296bb..0000000000 --- a/.github/workflows/node-package.yml +++ /dev/null @@ -1,235 +0,0 @@ -name: "node.js build" -on: - pull_request: - push: - tags: - - "*" - - "!py-*" - -jobs: - prebuild: - name: Prebuild - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, windows-latest] - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - uses: actions/setup-node@v4 - with: - node-version: "18" - - name: System info - run: | - rustc -vV - rustup -vV - cargo -vV - npm --version - node --version - - - name: Cache node modules - uses: actions/cache@v4 - with: - path: | - ${{ env.APPDATA }}/npm-cache - ~/.npm - key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git - target - key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2 - - - name: Install dependencies & build - if: steps.cache.outputs.cache-hit != 'true' - working-directory: node - run: npm install --verbose - - - name: Build Prebuild - working-directory: node - run: | - npm run prebuildify - tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds . - - - name: Upload Prebuild - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }} - path: node/${{ matrix.os }}.tar.gz - - prebuild-linux: - name: Prebuild Linux - runs-on: ubuntu-latest - - # Build Linux prebuilds inside a container with old glibc for backwards compatibility. - # Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6 - container: debian:10 - steps: - # Working directory is owned by 1001:1001 by default. - # Change it to our user. - - name: Change working directory owner - run: chown root:root . - - - uses: actions/checkout@v4 - with: - show-progress: false - - uses: actions/setup-node@v4 - with: - node-version: "18" - - run: apt-get update - - # Python is needed for node-gyp - - name: Install curl, python and compilers - run: apt-get install -y curl build-essential python3 - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: System info - run: | - rustc -vV - rustup -vV - cargo -vV - npm --version - node --version - - - name: Cache node modules - uses: actions/cache@v4 - with: - path: | - ${{ env.APPDATA }}/npm-cache - ~/.npm - key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git - target - key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2 - - - name: Install dependencies & build - if: steps.cache.outputs.cache-hit != 'true' - working-directory: node - run: npm install --verbose - - - name: Build Prebuild - working-directory: node - run: | - npm run prebuildify - tar -zcvf "linux.tar.gz" -C prebuilds . - - - name: Upload Prebuild - uses: actions/upload-artifact@v4 - with: - name: linux - path: node/linux.tar.gz - - pack-module: - needs: [prebuild, prebuild-linux] - name: Package deltachat-node and upload to download.delta.chat - runs-on: ubuntu-latest - steps: - - name: Install tree - run: sudo apt install tree - - uses: actions/checkout@v4 - with: - show-progress: false - - uses: actions/setup-node@v4 - with: - node-version: "18" - - name: Get tag - id: tag - uses: dawidd6/action-get-tag@v1 - continue-on-error: true - - name: Get Pull Request ID - id: prepare - run: | - tag=${{ steps.tag.outputs.tag }} - if [ -z "$tag" ]; then - node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV - else - echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV - echo "No preview will be uploaded this time, but the $tag release" - fi - - name: System info - run: | - rustc -vV - rustup -vV - cargo -vV - npm --version - node --version - echo $DELTACHAT_NODE_TAR_GZ - - name: Download Linux prebuild - uses: actions/download-artifact@v4 - with: - name: linux - - name: Download macOS prebuild - uses: actions/download-artifact@v4 - with: - name: macos-latest - - name: Download Windows prebuild - uses: actions/download-artifact@v4 - 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 - tree node/prebuilds - rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz - - name: Install dependencies without running scripts - run: | - npm install --ignore-scripts - - name: Build constants - run: | - npm run build:core:constants - - name: Build TypeScript part - run: | - npm run build:bindings:ts - - name: Package - shell: bash - run: | - mv node/README.md README.md - npm pack . - ls -lah - mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ - - name: Upload prebuild - uses: actions/upload-artifact@v4 - with: - name: deltachat-node.tgz - path: ${{ env.DELTACHAT_NODE_TAR_GZ }} - # Upload to download.delta.chat/node/preview/ - - name: Upload deltachat-node preview to download.delta.chat/node/preview/ - if: ${{ ! steps.tag.outputs.tag }} - id: upload-preview - shell: bash - run: | - echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE - chmod 600 __TEMP_INPUT_KEY_FILE - scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/" - continue-on-error: true - - name: Post links to details - if: steps.upload-preview.outcome == 'success' - run: node ./node/scripts/postLinksToDetails.js - env: - URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Upload to download.delta.chat/node/ - - name: Upload deltachat-node build to download.delta.chat/node/ - if: ${{ steps.tag.outputs.tag }} - id: upload - shell: bash - run: | - echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE - chmod 600 __TEMP_INPUT_KEY_FILE - scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/" diff --git a/.github/workflows/node-tests.yml b/.github/workflows/node-tests.yml deleted file mode 100644 index ae78784015..0000000000 --- a/.github/workflows/node-tests.yml +++ /dev/null @@ -1,68 +0,0 @@ -# GitHub Actions workflow -# to test Node.js bindings. - -name: "node.js tests" - -# Cancel previously started workflow runs -# when the branch is updated. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - pull_request: - push: - branches: - - main - -jobs: - tests: - name: Tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - uses: actions/setup-node@v4 - with: - node-version: "18" - - name: System info - run: | - rustc -vV - rustup -vV - cargo -vV - npm --version - node --version - - - name: Cache node modules - uses: actions/cache@v4 - with: - path: | - ${{ env.APPDATA }}/npm-cache - ~/.npm - key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/ - ~/.cargo/git - target - key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2 - - - name: Install dependencies & build - if: steps.cache.outputs.cache-hit != 'true' - working-directory: node - run: npm install --verbose - - - name: Test - timeout-minutes: 10 - working-directory: node - run: npm run test - env: - CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }} - NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true" diff --git a/.github/workflows/publish-deltachat-rpc-client-pypi.yml b/.github/workflows/publish-deltachat-rpc-client-pypi.yml index 986a949744..cdcbaced17 100644 --- a/.github/workflows/publish-deltachat-rpc-client-pypi.yml +++ b/.github/workflows/publish-deltachat-rpc-client-pypi.yml @@ -5,6 +5,8 @@ on: release: types: [published] +permissions: {} + jobs: build: name: Build distribution @@ -14,6 +16,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Install pypa/build run: python3 -m pip install build - name: Build a binary wheel and a source tarball diff --git a/.github/workflows/repl.yml b/.github/workflows/repl.yml index 0b8f38985c..fc5e881d8a 100644 --- a/.github/workflows/repl.yml +++ b/.github/workflows/repl.yml @@ -7,6 +7,8 @@ name: Build Windows REPL .exe on: workflow_dispatch: +permissions: {} + jobs: build_repl: name: Build REPL example @@ -15,8 +17,8 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build run: nix build .#deltachat-repl-win64 - name: Upload binary diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index df7a9ab843..19a9d62b1c 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -6,6 +6,8 @@ on: - main - build_jsonrpc_docs_ci +permissions: {} + jobs: build-rs: runs-on: ubuntu-latest @@ -14,6 +16,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Build the documentation with cargo run: | cargo doc --package deltachat --no-deps --document-private-items @@ -31,9 +34,9 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: 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 @@ -50,9 +53,9 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false fetch-depth: 0 # Fetch history to calculate VCS version number. - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build C documentation run: nix build .#docs - name: Upload to c.delta.chat @@ -72,6 +75,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false fetch-depth: 0 # Fetch history to calculate VCS version number. - name: Use Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/upload-ffi-docs.yml b/.github/workflows/upload-ffi-docs.yml index c79d61ed3d..8c2aab4059 100644 --- a/.github/workflows/upload-ffi-docs.yml +++ b/.github/workflows/upload-ffi-docs.yml @@ -9,6 +9,8 @@ on: branches: - main +permissions: {} + jobs: build: runs-on: ubuntu-latest @@ -17,6 +19,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + persist-credentials: false - name: Build the documentation with cargo run: | cargo doc --package deltachat_ffi --no-deps diff --git a/.github/workflows/zizmor-scan.yml b/.github/workflows/zizmor-scan.yml new file mode 100644 index 0000000000..bae5df50ea --- /dev/null +++ b/.github/workflows/zizmor-scan.yml @@ -0,0 +1,31 @@ +name: GitHub Actions Security Analysis with zizmor + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +jobs: + zizmor: + name: zizmor latest via PyPI + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 + + - name: Run zizmor + run: uvx zizmor --format sarif . > results.sarif + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.gitignore b/.gitignore index 479a3039e2..0be33f761e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -/target +target/ **/*.rs.bk /build /dist +/fuzz/fuzz_targets/corpus/ +/fuzz/fuzz_targets/crashes/ # ignore vi temporaries *~ @@ -51,3 +53,4 @@ result # direnv .envrc .direnv +.aider* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f60eb6912..cc0dd17fe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,569 @@ # Changelog +## [1.159.5] - 2025-05-14 + +### Fixes + +- Don't change webxdc self-addr when saving and loading draft ([#6854](https://github.com/chatmail/core/pull/6854)). + +### Miscellaneous Tasks + +- Remove duplicate miniz_oxide dependency. +- Update async-smtp to 0.10.2. + +## [1.159.4] - 2025-05-13 + +### Documentation + +- Add missing documentation to deltachat-rpc-client. + +### Features / Changes + +- Better avatar quality ([#6822](https://github.com/chatmail/core/pull/6822)). +- Update iroh from 0.33.0 to 0.35.0 ([#6687](https://github.com/chatmail/core/pull/6687)). +- Other dependency updates. + +### Fixes + +- Emit progress(0) in case AEAP is tried. +- Replace `FuturesUnordered` from `futures` with `JoinSet` from `tokio`. +- Fix order of operations when handling "vc-request-with-auth" ([#6850](https://github.com/chatmail/core/pull/6850)). +- Generate rfc724_mid when creating Message ([#6704](https://github.com/chatmail/core/pull/6704)) + +### Tests + +- Profile data is attached to group leave messages. + +## [1.159.3] - 2025-04-24 + +### CI + +- Use `ubuntu-latest` runner for `@deltachat/jsonrpc-client` publishing. + +## [1.159.2] - 2025-04-23 + +### Fixes + +- Allow to send to chats after failed securejoin again ([#6817](https://github.com/chatmail/core/pull/6817)). +- Parse login scheme in `add_transport_from_qr()` ([#6802](https://github.com/chatmail/core/pull/6802)). +- Lowercase address in add_transport() ([#6805](https://github.com/chatmail/core/pull/6805)). + +### API-Changes + +- Rename add_transport() -> add_or_update_transport() ([#6800](https://github.com/chatmail/core/pull/6800)). + +### Miscellaneous Tasks + +- Update yerpc to 0.6.4. +- Clean up `deltachat-jsonrpc` dependencies. + +### Refactor + +- Move logins into SQL table ([#6724](https://github.com/chatmail/core/pull/6724)). + +### Tests + +- Check headers absense straightforwardly. +- Fix mismatch between the contact and the account in securejoin tests. +- Test that key of the recipient is gossiped in 1:1 chats. + +## [1.159.1] - 2025-04-12 + +### API-Changes + +- deltachat-rpc-client: Add `Account.add_transport()`. +- Add jsonrpc for info_contact_id. + +### Build system + +- Update crossbeam-channel from 0.5.14 to 0.5.15. +- Increase MSRV to 1.82.0. + +### CI + +- Don't make ruff format quiet ([#6785](https://github.com/chatmail/core/pull/6785)). + +### Documentation + +- MimeFactory.member_timestamps has the same order as To: rather than RCPT TO:. +- Two JsonRPC doc improvements ([#6778](https://github.com/chatmail/core/pull/6778)). + +### Features / Changes + +- Improve error message when the user tries to do AEAP ([#6786](https://github.com/chatmail/core/pull/6786)). +- Pass email and password via env in python-jsonrpc. +- Track gossiping per (chat, fingerprint) pair. + +### Fixes + +- Add missing ChatDeleted event to python jsonrpc client. +- Never send Autocrypt-Gossip in broadcast lists. +- Restart I/O when mvbox_move setting is changed. + +### Tests + +- Port test_delete_deltachat_folder to JSON-RPC. +- Autocrypt-Gossip header isn't sent in broadcast messages. +- Encrypt test_subject_in_group(). +- Encrypt test_remove_member_bcc. + +## [1.159.0] - 2025-04-08 + +### API-Changes + +- deltachat-rpc-client: Add Message.get_info(). +- CFFI: Add `dc_make_vcard()` and `dc_import_vcard()`. +- Add legacy Python bindings for `make_vcard` and `import_vcard`. + +### CI + +- Upgrade Rust from 1.84.1 to 1.86.0 ([#6784](https://github.com/chatmail/core/pull/6784)). + +### Features / Changes + +- Add name resp. "Me" to contact encryption info ([#6720](https://github.com/chatmail/core/pull/6720)). +- Get contact-id for info messages ([#6714](https://github.com/chatmail/core/pull/6714)). +- No unencrypted chat when securejoin times out ([#6722](https://github.com/chatmail/core/pull/6722)). +- Clear `Param::IsEdited` when forwarding a message. +- Remove email address from 'add second device' qr code ([#6760](https://github.com/chatmail/core/pull/6760)). +- Parse Proton Mail vCards again ([#6771](https://github.com/chatmail/core/pull/6771)). +- Do not consider encrypting to the primary OpenPGP key. + +### Fixes + +- jsonrpc: Fix deadlock in get_all_accounts(). +- Set GroupNameTimestamp on group promotion ([#6729](https://github.com/chatmail/core/pull/6729)). +- Encrypt broadcast lists. + +### Miscellaneous Tasks + +- Update yerpc to 0.6.3. +- cargo: Update textwrap from 0.16.1 to 0.16.2. +- cargo: Bump uuid from 1.15.1 to 1.16.0. +- cargo: Bump libc from 0.2.170 to 0.2.171. +- cargo: Bump anyhow from 1.0.96 to 1.0.97. +- cargo: Bump bytes from 1.10.0 to 1.10.1. +- cargo: Bump once_cell from 1.20.3 to 1.21.3. +- cargo: Bump thiserror from 2.0.11 to 2.0.12. +- cargo: Bump pin-project from 1.1.9 to 1.1.10. +- cargo: Bump hyper-util from 0.1.10 to 0.1.11. +- cargo: Bump log from 0.4.26 to 0.4.27. +- cargo: Bump tokio-util from 0.7.13 to 0.7.14. +- cargo: Bump syn from 2.0.98 to 2.0.100. +- cargo: Bump serde_json from 1.0.139 to 1.0.140. +- cargo: Bump quote from 1.0.38 to 1.0.40. +- cargo: Bump http-body-util from 0.1.2 to 0.1.3. +- cargo: Bump openssl from 0.10.71 to 0.10.72. +- cargo: Bump quick-xml from 0.37.2 to 0.37.4. +- cargo: Bump blake3 from 1.6.1 to 1.8.0. +- cargo: Bump tokio from 1.43.0 to 1.43.1 ([#6780](https://github.com/chatmail/core/pull/6780)). +- Add issue template. +- Add bug label on bug issue template. +- cargo: Bump tokio from 1.43.0 to 1.44.1. +- cargo: Bump fd-lock from 4.0.2 to 4.0.4. +- Update async-smtp from 0.10.0 to 0.10.1. +- Update async-imap from 0.10.3 to 0.10.4. +- cargo: Bump tempfile from 3.14.0 to 3.19.1. +- cargo: Bump image from 0.25.5 to 0.25.6. +- cargo: Bump serde from 1.0.218 to 1.0.219. + +### Other + +- Add python and tox to flake.nix devshell ([#6233](https://github.com/chatmail/core/pull/6233)) +- Update spec wrt edit/delete, minor rewordings ([#6708](https://github.com/chatmail/core/pull/6708)) +- Update 'takes longer' fallback wording. +- Handle classic emails as such only in classic profiles ([#6767](https://github.com/chatmail/core/pull/6767)) +- Move ASM strings to core, point to "Add Second Device" ([#6777](https://github.com/chatmail/core/pull/6777)) + +### Refactor + +- Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`. +- Move vCard code to its own file ([#6776](https://github.com/chatmail/core/pull/6776)). + +### Tests + +- Use encryption in more Rust tests. +- Use encryption in all JSON-RPC online tests. +- Encrypt legacy Python tests. +- Send only encrypted messages in online JS tests. +- Add APIs to create `dom@example.net` and `elena@example.net`. +- Split public keys from secret keys in runtime. +- Remove fetch_existing tests. +- Port test_forward_encrypted_to_unencrypted from legacy Python to Rust. +- Port test_one_account_send_bcc_setting from legacy Python to JSON-RPC. +- Port test_multidevice_sync_seen to JSON-RPC. +- Use QR codes to setup contact with test bots. +- Remove flaky key::tests::test_load_self_existing test ([#6763](https://github.com/chatmail/core/pull/6763)). +- Update blob hash in blob::blob_tests::test_selfavatar_outside_blobdir. + +## [1.158.0] - 2025-03-29 + +### API-Changes + +- deltachat-rpc-client: Accept `Account` as `Account.create_contact()` argument. +- Rust: Add `ContactId.set_name()`. +- JSON-RPC: Rename parameter name in `get_webxdc_href` to `info_msg_id` to reduce confusion potential ([#6681](https://github.com/chatmail/core/pull/6681)). + +### Features / Changes + +- Nicer configuration error ([#6684](https://github.com/chatmail/core/pull/6684)). +- securejoin: Do not create 1:1 chat on Alice's side until `vc-request-with-auth`. +- Understandable error message when accounts.lock can't be locked ([#6695](https://github.com/chatmail/core/pull/6695)). +- Simplify e2ee decision logic, remove majority vote. +- Stop saving txt_raw. + +### Fixes + +- Do not fail to send the message if some keys are missing. +- Synchronize contact name changes. +- Move group name timestamp update up in create_send_msg_jobs(). +- Fixes for transport JSON-RPC ([#6680](https://github.com/chatmail/core/pull/6680)). + +### Build system + +- deltachat-rpc-client: Move development dependencies from tox.ini to pyproject.toml. +- Update resolve-conf from 0.7.0 to 0.7.1. + +### Refactor + +- Do not convert SQL arguments to `String` unnecessarily. +- Factor out `update_chat_names()`. +- Use `created_timestamp()` instead of duplicating its code ([#6692](https://github.com/chatmail/core/pull/6692)). +- Use `chat_id.get_timestamp()` instead of duplicating its code ([#6691](https://github.com/chatmail/core/pull/6691)). +- Move `mark_recipients_as_verified()` call out of `has_verified_encryption()`. +- Move `proxy_config` out of `ConfiguredLoginParam` ([#6712](https://github.com/chatmail/core/pull/6712)). + +### Tests + +- Use vCard in TestContext.add_or_lookup_contact(). +- Remove test_group_with_removed_message_id. +- Use add_or_lookup_email_contact() in get_chat(). +- Use add_or_lookup_email_contact in test_setup_contact_ex. +- Use vCards more in Python tests. +- Use TestContextManager in more tests. +- Use vCards to create contacts in more Rust tests. +- Set chat name multiple times in a row. +- Online test for renaming the group multiple times. + +## [1.157.3] - 2025-03-19 + +### API-Changes + +- jsonrpc: Add `copy_to_blob_dir` api ([#6660](https://github.com/chatmail/core/pull/6660)). +- Add "delete_for_all" function in json-rpc ([#6672](https://github.com/chatmail/core/pull/6672)). +- Sketch add_transport_from_qr(), add_transport(), list_transports(), delete_transport() APIs ([#6589](https://github.com/chatmail/core/pull/6589)). + +### Build system + +- Remove websocket support from deltachat-jsonrpc. +- Remove encoded-words dependency. + +### Fixes + +- Never send empty `To:` header ([#6663](https://github.com/chatmail/core/pull/6663)). +- Use protected `Date` header for signed messages. +- Fix setting up a profile and immediately transferring to a second device ([#6657](https://github.com/chatmail/core/pull/6657)). +- Don't SMTP-send self-only messages if DeleteServerAfter is "immediate" ([#6661](https://github.com/chatmail/core/pull/6661)). +- Use protected `Date` with protected Autocrypt. + +### Miscellaneous Tasks + +- cargo: Bump uuid from 1.12.1 to 1.15.1. +- Update `strum` dependency. + +### Refactor + +- deltachat-rpc-client: Use wait_for_event() type argument. + +### Tests + +- Avoid creating contacts in `test_sync_{accept,block}_before_first_msg()`. +- Fix `test_no_old_msg_is_fresh` flakiness. + +## [1.157.2] - 2025-03-15 + +### Fixes + +- Prefer hidden Message-ID header if any. +- Update async-compression to 0.4.21 to fix IMAP COMPRESS getting stuck. + +### Refactor + +- Extract handle_edit_delete() function for message edit/delete ([#6664](https://github.com/chatmail/core/pull/6664)). + +### Tests + +- test_secure_join: Bob should not create a 1:1 chat before sending a message. +- Return chat ID from TestContext.exec_securejoin_qr(). + +## [1.157.1] - 2025-03-13 + +### Miscellaneous Tasks + +- Update repository URLs to make npm and PyPI publishing possible. + +## [1.157.0] - 2025-03-12 + +### Features / Changes + +- Ignore encryption preferences. + +### API-Changes + +- deltachat-rpc-client: Make it possible to clone accounts. +- deltachat-rpc-client: Add Account.device_contact. +- deltachat-rpc-client: Add Account.get_device_chat(). +- deltechat-rpc-client: Add Account.wait_for_msgs_noticed_event(). +- ffi: Store reference pointer to Context in dc_chat_t. + +### Build system + +- Intergrate `fuzz` crate into workspace. +- Update env_logger to get rid of unmaintained humantime dependency. +- nix: Update NDK to 27.2.12479018. +- Build Android wheels for PyPI. + +### Documentation + +- deltachat-rpc-client: Document Account.check_qr(). +- deltachat-rpc-client: Document Account.import_vcard(). + +### Fixes + +- Update async-imap to 0.10.23 to fix division by zero. +- Ignore hidden headers in IMF section. +- Process Autocrypt-Gossip only after merging protected headers. + +### Miscellaneous Tasks + +- cargo: Bump smallvec from 1.13.2 to 1.14.0. + +### Tests + +- Deletion request fails in an unencrypted chat and the message remains. +- python: port `test_no_old_msg_is_fresh` to JSON-RPC. + +## [1.156.3] - 2025-03-09 + +### API-Changes + +- jsonrpc: Add import_vcard_contents() method. +- jsonrpc: Add API to make and import vCards. +- [**breaking**] Remove save_mime_headers config option and dc_get_mime_headers(). +- [**breaking**] Remove key_gen_type config. + +### Features / Changes + +- Add chat-deleted event. +- Delete messages on IMAP when deleting chat ([#6613](https://github.com/chatmail/core/pull/6613)). +- Allow doubled avatar resolution + +### Fixes + +- Move Chat-Group-Avatar to hidden headers. +- Ignore outer Chat-User-Avatar header in Autocrypt-encrypted messages. + +### Build system + +- Use mailbuilder from crates.io. +- Update iroh to 0.33. + +### Documentation + +- Nonstandard headers needing DKIM protection should be hidden. + +### Refactor + +- Recode_to_size(): Rename strict_limits to is_avatar. + +### Tests + +- Test for ChatDeleted event. +- Replace create_chat() with get_chat() in test_setup_contact_ex() and test_secure_join(). +- Transfer vCards in TestContext.create_chat(). + +## [1.156.2] - 2025-03-02 + +### Fixes + +- Upgrade native-tls from 0.2.13 to 0.2.14. This fixes "Accept invalid certificates" failing on Android with "OpenSSL error". The bug was there since 1.156.0 due to upgrade of native-tls from 0.2.11 to 0.2.13. + +### Features / Changes + +- Show sender name in 'Saved Messages' summary ([#6607](https://github.com/chatmail/core/pull/6607)). +- Sync chats deletion across devices. + +### Documentation + +- Add DC_QR_BACKUP_TOO_NEW documentation. + +### Miscellaneous Tasks + +- cargo: Bump anyhow from 1.0.95 to 1.0.96. +- cargo: Bump serde from 1.0.217 to 1.0.218. + +## [1.156.1] - 2025-02-28 + +### Fixes + +- Update mailparse to 0.16.1 to fix panic when parsing a message. +- Add Chat-Group-Name-Timestamp header and use it to update group names ([#6412](https://github.com/chatmail/core/pull/6412)). +- Log tokio::fs::metadata errors. + +### Build system + +- Update fuzzing setup. + +## [1.156.0] - 2025-02-26 + +### API-Changes + +- Save messages API in JSON RPC ([#6554](https://github.com/chatmail/core/pull/6554)). +- jsonrpc: Add `MessageObject.is_edited`. +- jsonrpc: Add `send_edit_request`. +- Deduplicate blob files in the JsonRPC API ([#6470](https://github.com/chatmail/core/pull/6470)). +- Message deletion request API ([#6576](https://github.com/chatmail/core/pull/6576)) + +### Features / Changes + +- Edit message's text ([#6550](https://github.com/chatmail/core/pull/6550)) +- Sync message deletion to other devices ([#6573](https://github.com/chatmail/core/pull/6573)) +- Allow scanning multiple securejoin QR codes in parallel. +- When reactions are seen, remove notification from second device ([#6480](https://github.com/chatmail/core/pull/6480)). +- Enable bcc-self automatically when doing Autocrypt Setup Message. +- Don't send a notification when a group member left ([#6575](https://github.com/chatmail/core/pull/6575)). +- Fail on too new backups ([#6580](https://github.com/chatmail/core/pull/6580)). + +### Fixes + +- Make it impossible to overwrite default key. +- Do not allow to edit html messages ([#6564](https://github.com/chatmail/core/pull/6564)). +- `get_config(Config::Selfavatar)` returns the path, not the name ([#6570](https://github.com/chatmail/core/pull/6570)). +- `chat::save_msgs`: Interrupt inbox loop to send a sync message. +- Do not delete files if cannot read their metadata. + +### Build system + +- nix: Update hashes of git dependencies. +- Update some dependencies. + +### CI + +- Remove deprecated DeterminateSystems/magic-nix-cache-action. + +### Refactor + +- Use mail-builder instead of lettre_email. +- Move even even more tests into their own files ([#6559](https://github.com/chatmail/core/pull/6559)). +- Remove `Message.set_file()`, `dc_msg_set_file()` and related code ([#6558](https://github.com/chatmail/core/pull/6558)). +- Remove unused blob functions ([#6563](https://github.com/chatmail/core/pull/6563)). +- Let `BlobObject::from_name()` take `&str` ([#6571](https://github.com/chatmail/core/pull/6571)). +- Don't use traits where it's not necessary ([#6567](https://github.com/chatmail/core/pull/6567)). + +## [1.155.6] - 2025-02-17 + +### Features / Changes + +- Sort past members by the timestamp of removal. +- Use UUID v4 to generate Message-IDs. + +### Fixes + +- Use dedicated ID for sync messages affecting device chat. +- Do not allow non-members to change ephemeral timer settings. +- Show padlock when the message is not sent over the network. + +### Build system + +- Remove deprecated node module. + +### CI + +- Audit workflows with zizmor. + +### Documentation + +- Improve docstrings ([#6496](https://github.com/chatmail/core/pull/6496)). + +## [1.155.5] - 2025-02-14 + +### Fixes + +- Get_filename() is now guaranteed to return a valid filename ([#6537](https://github.com/chatmail/core/pull/6537)). + +### Miscellaneous Tasks + +- Add RUSTSEC-2025-0006 to deny.toml. + +### Refactor + +- Do not cancel the task returned from async_imap `Handle.wait_with_timeout`. + +## [1.155.4] - 2025-02-10 + +### CI + +- Upgrade Rust from 1.84.0 to 1.84.1. + +### Fixes + +- Use CRLF newlines in vCards. +- Make vCard parsing more robust in case of trailing newlines. +- Do not include CRLF before MIME boundary in the part body. +- Accept QR codes with 'broken' JSON ([#6528](https://github.com/chatmail/core/pull/6528)). + +### Other + +- Add `MessageQuote.chat_id`. + +### Refactor + +- Move even more tests into their own files ([#6521](https://github.com/chatmail/core/pull/6521)). + +## [1.155.3] - 2025-02-05 + +### Fixes + +- Store device token in IMAP METADATA on each connection. + +### Miscellaneous Tasks + +- Upgrade iroh from 0.30 to 0.32. +- Update `pgp` to 0.15. +- cargo: Bump thiserror from 1.0.69 to 2.0.9. +- cargo: Bump pin-project from 1.1.7 to 1.1.8. +- cargo: Bump dirs from 5.0.1 to 6.0.0. +- cargo: Bump hyper from 1.5.2 to 1.6.0. +- cargo: Bump webpki-roots from 0.26.7 to 0.26.8. +- cargo: Bump futures-lite from 2.5.0 to 2.6.0. +- Update OpenSSL to fix RUSTSEC-2025-0004. +- cargo: Bump tokio from 1.42.0 to 1.43.0. +- cargo: Bump syn from 2.0.94 to 2.0.98. +- cargo: Bump rustls from 0.23.20 to 0.23.22. +- cargo: Bump data-encoding from 2.6.0 to 2.7.0. +- cargo: Bump serde_json from 1.0.134 to 1.0.138. +- cargo: Bump uuid from 1.11.0 to 1.12.1. +- cargo: Bump log from 0.4.22 to 0.4.25. +- cargo: Bump rustls-pki-types from 1.10.1 to 1.11.0. +- Update futures-concurrency. + +### Documentation + +- Assign docs to correct object. + +### Tests + +- Make sure DCBACKUP2 compatibility does not break again. + ## [1.155.2] - 2025-01-31 +This release accidentally broke compatibility +with previous versions of `DCBACKUP2` QR codes +due to iroh upgrade. + ### API-Changes -- Add `IncomingReaction.chat_id` ([#6459](https://github.com/deltachat/deltachat-core-rust/pull/6459)). +- Add `IncomingReaction.chat_id` ([#6459](https://github.com/chatmail/core/pull/6459)). ### Features / Changes @@ -16,11 +575,11 @@ - Don't remove file extension when recoding avatars. - Use `BufReader` when reading .xdc files. -- No implicit member changes when we are added to the group ([#6493](https://github.com/deltachat/deltachat-core-rust/pull/6493)). +- No implicit member changes when we are added to the group ([#6493](https://github.com/chatmail/core/pull/6493)). ### Documentation -- jsonrpc: Update documentation for `select_account` and `get_selected_account_id` ([#6483](https://github.com/deltachat/deltachat-core-rust/pull/6483)). +- jsonrpc: Update documentation for `select_account` and `get_selected_account_id` ([#6483](https://github.com/chatmail/core/pull/6483)). - jsonrpc: Add docs for some functions. ## [1.155.1] - 2025-01-25 @@ -38,8 +597,8 @@ ### Refactor -- Remove BlobObject::create(), use create_and_deduplicate_from_bytes() instead ([#6467](https://github.com/deltachat/deltachat-core-rust/pull/6467)). -- Move more tests into their own files ([#6473](https://github.com/deltachat/deltachat-core-rust/pull/6473)). +- Remove BlobObject::create(), use create_and_deduplicate_from_bytes() instead ([#6467](https://github.com/chatmail/core/pull/6467)). +- Move more tests into their own files ([#6473](https://github.com/chatmail/core/pull/6473)). ## [1.155.0] - 2025-01-23 @@ -54,13 +613,13 @@ ### Features / Changes -- feat: Set BccSelf to true when receiving a sync message ([#6434](https://github.com/deltachat/deltachat-core-rust/pull/6434)) -- File deduplication ([#6332](https://github.com/deltachat/deltachat-core-rust/pull/6332)) +- feat: Set BccSelf to true when receiving a sync message ([#6434](https://github.com/chatmail/core/pull/6434)) +- File deduplication ([#6332](https://github.com/chatmail/core/pull/6332)) ### Refactor - Move tests to their own files. -- Extract `group_changes_msgs()` function ([#6460](https://github.com/deltachat/deltachat-core-rust/pull/6460)). +- Extract `group_changes_msgs()` function ([#6460](https://github.com/chatmail/core/pull/6460)). ## [1.154.3] - 2025-01-20 @@ -71,17 +630,17 @@ ### Miscellaneous Tasks -- Remove unused function delete_files_in_dir() ([#6454](https://github.com/deltachat/deltachat-core-rust/pull/6454)). +- Remove unused function delete_files_in_dir() ([#6454](https://github.com/chatmail/core/pull/6454)). ## [1.154.2] - 2025-01-20 ### Features / Changes -- Add API to save messages ([#5606](https://github.com/deltachat/deltachat-core-rust/pull/5606)). +- Add API to save messages ([#5606](https://github.com/chatmail/core/pull/5606)). ### Fixes -- fix: Don't accidentally remove Self from groups ([#6455](https://github.com/deltachat/deltachat-core-rust/pull/6455)). +- fix: Don't accidentally remove Self from groups ([#6455](https://github.com/chatmail/core/pull/6455)). - Do not create tombstones for members removed from unpromoted groups. ### Build system @@ -106,8 +665,8 @@ ### Fixes -- Migration: Set bcc_self=1 if it's unset and delete_server_after!=1 ([#6432](https://github.com/deltachat/deltachat-core-rust/pull/6432)). -- Clear the config cache after every migration ([#6438](https://github.com/deltachat/deltachat-core-rust/pull/6438)). +- Migration: Set bcc_self=1 if it's unset and delete_server_after!=1 ([#6432](https://github.com/chatmail/core/pull/6432)). +- Clear the config cache after every migration ([#6438](https://github.com/chatmail/core/pull/6438)). ### Build system @@ -120,14 +679,14 @@ ### Miscellaneous Tasks -- Beta Clippy suggestions ([#6422](https://github.com/deltachat/deltachat-core-rust/pull/6422)). +- Beta Clippy suggestions ([#6422](https://github.com/chatmail/core/pull/6422)). ### Refactor - Use let..else. - Add why_cant_send_ex() capable to only ignore specified conditions. - Remove unnecessary is_contact_in_chat check. -- Eliminate remaining repeat_vars() calls ([#6359](https://github.com/deltachat/deltachat-core-rust/pull/6359)). +- Eliminate remaining repeat_vars() calls ([#6359](https://github.com/chatmail/core/pull/6359)). ### Tests @@ -137,24 +696,24 @@ ### Features / Changes -- Remove "jobs" from imap_markseen if folder doesn't exist ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)). -- Delete `vg-request-with-auth` from IMAP after processing ([#6208](https://github.com/deltachat/deltachat-core-rust/pull/6208)). +- Remove "jobs" from imap_markseen if folder doesn't exist ([#5870](https://github.com/chatmail/core/pull/5870)). +- Delete `vg-request-with-auth` from IMAP after processing ([#6208](https://github.com/chatmail/core/pull/6208)). ### API-Changes -- Add `IncomingWebxdcNotify.chat_id` ([#6356](https://github.com/deltachat/deltachat-core-rust/pull/6356)). -- rpc-client: Add INCOMING_REACTION to const.EventType ([#6349](https://github.com/deltachat/deltachat-core-rust/pull/6349)). +- Add `IncomingWebxdcNotify.chat_id` ([#6356](https://github.com/chatmail/core/pull/6356)). +- rpc-client: Add INCOMING_REACTION to const.EventType ([#6349](https://github.com/chatmail/core/pull/6349)). ### Documentation -- Viewtype::Sticker may be changed to Image and how to disable that ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)). +- Viewtype::Sticker may be changed to Image and how to disable that ([#6352](https://github.com/chatmail/core/pull/6352)). ### Fixes -- Never change Viewtype::Sticker to Image if file has non-image extension ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)). -- Change BccSelf default to 0 for chatmail ([#6340](https://github.com/deltachat/deltachat-core-rust/pull/6340)). +- Never change Viewtype::Sticker to Image if file has non-image extension ([#6352](https://github.com/chatmail/core/pull/6352)). +- Change BccSelf default to 0 for chatmail ([#6340](https://github.com/chatmail/core/pull/6340)). - Mark holiday notice messages as bot-generated. -- Don't treat location-only and sync messages as bot ones ([#6357](https://github.com/deltachat/deltachat-core-rust/pull/6357)). +- Don't treat location-only and sync messages as bot ones ([#6357](https://github.com/chatmail/core/pull/6357)). - Update shadowsocks crate to 1.22.0 to avoid panic when parsing some QR codes. - Prefer to encrypt if E2eeEnabled even if peers have EncryptPreference::NoPreference. - Prioritize mailing list over self-sent messages. @@ -163,7 +722,7 @@ ### Refactor -- Remove unused parameter and return value from `build_body_file(…)` ([#6369](https://github.com/deltachat/deltachat-core-rust/pull/6369)). +- Remove unused parameter and return value from `build_body_file(…)` ([#6369](https://github.com/chatmail/core/pull/6369)). - Deprecate Param::ErroneousE2ee. - Add `emit_msgs_changed_without_msg_id`. - Add_parts: Remove excessive `is_mdn` checks. @@ -189,7 +748,7 @@ ### Fixes - Reduce number of `repeat_vars()` calls. -- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/deltachat/deltachat-core-rust/pull/6362)). +- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/chatmail/core/pull/6362)). ### Refactor @@ -220,11 +779,11 @@ - Cache HTTP GET requests. - Prefix server-url in info. -- Set `mime_modified` for the last message part, not the first ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)). +- Set `mime_modified` for the last message part, not the first ([#4462](https://github.com/chatmail/core/pull/4462)). ### Fixes -- Render "message" parts in multipart messages' HTML ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)). +- Render "message" parts in multipart messages' HTML ([#4462](https://github.com/chatmail/core/pull/4462)). - Ignore garbage at the end of the keys. ## [1.151.6] - 2024-12-11 @@ -232,7 +791,7 @@ ### Features / Changes - Don't add "Failed to send message to ..." info messages to group chats. -- Add info messages about implicit membership changes if group member list is recreated ([#6314](https://github.com/deltachat/deltachat-core-rust/pull/6314)). +- Add info messages about implicit membership changes if group member list is recreated ([#6314](https://github.com/chatmail/core/pull/6314)). ### Fixes @@ -253,7 +812,7 @@ ### Tests -- Notifiy more prominently & in more tests about false positives when running `cargo test` ([#6308](https://github.com/deltachat/deltachat-core-rust/pull/6308)). +- Notifiy more prominently & in more tests about false positives when running `cargo test` ([#6308](https://github.com/chatmail/core/pull/6308)). ## [1.151.5] - 2024-12-05 @@ -273,7 +832,7 @@ ### Fixes -- Store plaintext in mime_headers of truncated sent messages ([#6273](https://github.com/deltachat/deltachat-core-rust/pull/6273)). +- Store plaintext in mime_headers of truncated sent messages ([#6273](https://github.com/chatmail/core/pull/6273)). ### Documentation @@ -282,7 +841,7 @@ ### Tests -- Fix panic in `receive_emails` benchmark ([#6306](https://github.com/deltachat/deltachat-core-rust/pull/6306)). +- Fix panic in `receive_emails` benchmark ([#6306](https://github.com/chatmail/core/pull/6306)). ## [1.151.4] - 2024-12-03 @@ -296,7 +855,7 @@ ### Miscellaneous Tasks -- Beta clippy suggestions ([#6271](https://github.com/deltachat/deltachat-core-rust/pull/6271)). +- Beta clippy suggestions ([#6271](https://github.com/chatmail/core/pull/6271)). ### Tests @@ -311,7 +870,7 @@ ### API-Changes - Remove experimental `request_internet_access` option from webxdc's `manifest.toml`. -- Add getWebxdcHref to json api ([#6281](https://github.com/deltachat/deltachat-core-rust/pull/6281)). +- Add getWebxdcHref to json api ([#6281](https://github.com/chatmail/core/pull/6281)). ### CI @@ -319,13 +878,13 @@ ### Documentation -- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/deltachat/deltachat-core-rust/pull/6269)). +- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/chatmail/core/pull/6269)). - Fix references to iroh-related headers in peer_channels docs. - Improve CFFI docs, link to corresponding JSON-RPC docs. ### Features / Changes -- Allow the user to replace maps integration ([#5678](https://github.com/deltachat/deltachat-core-rust/pull/5678)). +- Allow the user to replace maps integration ([#5678](https://github.com/chatmail/core/pull/5678)). - Mark saved messages chat as protected. ### Fixes @@ -333,7 +892,7 @@ - Close iroh endpoint when I/O is stopped. - Do not add protection messages to Saved Messages chat. - Mark Saved Messages chat as protected if it exists. -- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/deltachat/deltachat-core-rust/pull/6259)). +- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/chatmail/core/pull/6259)). ### Refactor @@ -345,15 +904,15 @@ ### API-Changes -- Deprecate webxdc `descr` parameter ([#6255](https://github.com/deltachat/deltachat-core-rust/pull/6255)). +- Deprecate webxdc `descr` parameter ([#6255](https://github.com/chatmail/core/pull/6255)). ### Features / Changes - AEAP: Check that the old peerstate verified key fingerprint hasn't changed when removing it. -- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/deltachat/deltachat-core-rust/pull/6118)). -- Do not use format=flowed in outgoing messages ([#6256](https://github.com/deltachat/deltachat-core-rust/pull/6256)). +- Add `AccountsChanged` and `AccountsItemChanged` events ([#6118](https://github.com/chatmail/core/pull/6118)). +- Do not use format=flowed in outgoing messages ([#6256](https://github.com/chatmail/core/pull/6256)). - Add webxdc limits api. -- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/deltachat/deltachat-core-rust/pull/6266)). +- Add href to IncomingWebxdcNotify event ([#6266](https://github.com/chatmail/core/pull/6266)). ### Fixes @@ -382,64 +941,64 @@ ### Features / Changes - Trim whitespace from scanned QR codes. -- Use privacy-preserving webxdc addresses ([#6237](https://github.com/deltachat/deltachat-core-rust/pull/6237)). -- Webxdc notify ([#6230](https://github.com/deltachat/deltachat-core-rust/pull/6230)). -- `update.href` api ([#6248](https://github.com/deltachat/deltachat-core-rust/pull/6248)). +- Use privacy-preserving webxdc addresses ([#6237](https://github.com/chatmail/core/pull/6237)). +- Webxdc notify ([#6230](https://github.com/chatmail/core/pull/6230)). +- `update.href` api ([#6248](https://github.com/chatmail/core/pull/6248)). ### Fixes -- Never notify SELF ([#6251](https://github.com/deltachat/deltachat-core-rust/pull/6251)). +- Never notify SELF ([#6251](https://github.com/chatmail/core/pull/6251)). ### Build system - Use underscores in deltachat-rpc-server source package filename. -- Remove imap_tools from dependencies ([#6238](https://github.com/deltachat/deltachat-core-rust/pull/6238)). +- Remove imap_tools from dependencies ([#6238](https://github.com/chatmail/core/pull/6238)). - cargo: Update Rustls from 0.23.14 to 0.23.18. - deps: Bump curve25519-dalek from 3.2.0 to 4.1.3 in /fuzz. ### Documentation - Move style guide into a separate document. -- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/deltachat/deltachat-core-rust/pull/6249)). +- Clarify DC_EVENT_INCOMING_WEBXDC_NOTIFY documentation ([#6249](https://github.com/chatmail/core/pull/6249)). ### Tests -- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/deltachat/deltachat-core-rust/pull/6222)). +- After AEAP, 1:1 chat isn't available for sending, but unprotected groups are ([#6222](https://github.com/chatmail/core/pull/6222)). ## [1.150.0] - 2024-11-21 ### API-Changes -- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/deltachat/deltachat-core-rust/pull/6176)). +- Correct `DC_CERTCK_ACCEPT_*` values and docs ([#6176](https://github.com/chatmail/core/pull/6176)). ### Features / Changes -- Use Rustls for connections with strict TLS ([#6186](https://github.com/deltachat/deltachat-core-rust/pull/6186)). +- Use Rustls for connections with strict TLS ([#6186](https://github.com/chatmail/core/pull/6186)). - Experimental header protection for Autocrypt. - Tune down io-not-started info in connectivity-html. -- Clear config cache in start_io() ([#6228](https://github.com/deltachat/deltachat-core-rust/pull/6228)). +- Clear config cache in start_io() ([#6228](https://github.com/chatmail/core/pull/6228)). - Line-before-quote may be up to 120 character long instead of 80. -- Use i.delta.chat in qr codes ([#6223](https://github.com/deltachat/deltachat-core-rust/pull/6223)). +- Use i.delta.chat in qr codes ([#6223](https://github.com/chatmail/core/pull/6223)). ### Fixes -- Prevent accidental wrong-password-notifications ([#6122](https://github.com/deltachat/deltachat-core-rust/pull/6122)). +- Prevent accidental wrong-password-notifications ([#6122](https://github.com/chatmail/core/pull/6122)). - Remove footers from "Show Full Message...". - `send_msg_to_smtp`: Return Ok if `smtp` row is deleted in parallel. -- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/deltachat/deltachat-core-rust/pull/5992)). +- Only add "member added/removed" messages if they actually do that ([#5992](https://github.com/chatmail/core/pull/5992)). - Do not fail to load chatlist summary if the message got removed. - deltachat-jsonrpc: Do not fail `get_chatlist_items_by_entries` if the message got deleted. - deltachat-jsonrpc: Do not fail `get_draft` if draft is deleted. -- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)). +- `markseen_msgs`: Limit not yet downloaded messages state to `InNoticed` ([#2970](https://github.com/chatmail/core/pull/2970)). - Update state of message when fully downloading it. -- Dont overwrite equal drafts ([#6212](https://github.com/deltachat/deltachat-core-rust/pull/6212)). +- Dont overwrite equal drafts ([#6212](https://github.com/chatmail/core/pull/6212)). ### Build system - Silence RUSTSEC-2024-0384. - cargo: Update rPGP from 0.13.2 to 0.14.0. - cargo: Update futures-concurrency from 7.6.1 to 7.6.2. -- Update flake.nix ([#6200](https://github.com/deltachat/deltachat-core-rust/pull/6200)) +- Update flake.nix ([#6200](https://github.com/chatmail/core/pull/6200)) ### CI @@ -474,8 +1033,8 @@ - Remove all calls to print() from deltachat-rpc-client tests. - Reply to protected group from MUA. -- Mark not downloaded message as seen ([#2970](https://github.com/deltachat/deltachat-core-rust/pull/2970)). -- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/deltachat/deltachat-core-rust/pull/6235)). +- Mark not downloaded message as seen ([#2970](https://github.com/chatmail/core/pull/2970)). +- Mark `receive_imf()` as only for tests and "internals" feature ([#6235](https://github.com/chatmail/core/pull/6235)). ## [1.149.0] - 2024-11-05 @@ -503,9 +1062,9 @@ - send_msg_to_smtp: Do not fail if the message does not exist anymore. - Do not percent-encode dot when passing to autoconfig server. -- Save contact name from SecureJoin QR to `authname`, not to `name` ([#6115](https://github.com/deltachat/deltachat-core-rust/pull/6115)). +- Save contact name from SecureJoin QR to `authname`, not to `name` ([#6115](https://github.com/chatmail/core/pull/6115)). - Always exit fake IDLE after at most 60 seconds. -- Concat NDNs ([#6129](https://github.com/deltachat/deltachat-core-rust/pull/6129)). +- Concat NDNs ([#6129](https://github.com/chatmail/core/pull/6129)). ### Refactor @@ -519,34 +1078,34 @@ ### API-Changes -- Add Message::new_text() ([#6123](https://github.com/deltachat/deltachat-core-rust/pull/6123)). -- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/deltachat/deltachat-core-rust/pull/6120)). +- Add Message::new_text() ([#6123](https://github.com/chatmail/core/pull/6123)). +- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/chatmail/core/pull/6120)). ### Features / Changes -- Enable Webxdc realtime by default ([#6125](https://github.com/deltachat/deltachat-core-rust/pull/6125)). +- Enable Webxdc realtime by default ([#6125](https://github.com/chatmail/core/pull/6125)). ### Fixes -- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/deltachat/deltachat-core-rust/pull/6091)). -- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/deltachat/deltachat-core-rust/pull/6121)). -- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/deltachat/deltachat-core-rust/pull/6130)). +- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/chatmail/core/pull/6091)). +- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/chatmail/core/pull/6121)). +- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/chatmail/core/pull/6130)). ### Miscellaneous Tasks -- Silence another rust-analyzer false-positive ([#6124](https://github.com/deltachat/deltachat-core-rust/pull/6124)). +- Silence another rust-analyzer false-positive ([#6124](https://github.com/chatmail/core/pull/6124)). - cargo: Upgrade iroh to 0.26.0. ### Refactor -- Directly use connectives ([#6128](https://github.com/deltachat/deltachat-core-rust/pull/6128)). -- Use Message::new_text() more ([#6127](https://github.com/deltachat/deltachat-core-rust/pull/6127)). +- Directly use connectives ([#6128](https://github.com/chatmail/core/pull/6128)). +- Use Message::new_text() more ([#6127](https://github.com/chatmail/core/pull/6127)). ## [1.148.5] - 2024-10-27 ### Fixes -- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/deltachat/deltachat-core-rust/pull/5896)). +- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/chatmail/core/pull/5896)). - Do not take write lock for maybe_network_lost() and set_push_device_token(). - Do not lock the account manager for the whole duration of background_fetch. @@ -571,7 +1130,7 @@ ### Features / Changes -- Jsonrpc: add `private_tag` to `Account::Configured` Object ([#6107](https://github.com/deltachat/deltachat-core-rust/pull/6107)). +- Jsonrpc: add `private_tag` to `Account::Configured` Object ([#6107](https://github.com/chatmail/core/pull/6107)). ### Fixes @@ -594,7 +1153,7 @@ ### Documentation -- Fix DC_QR_PROXY docs ([#6099](https://github.com/deltachat/deltachat-core-rust/pull/6099)). +- Fix DC_QR_PROXY docs ([#6099](https://github.com/chatmail/core/pull/6099)). ### Refactor @@ -631,11 +1190,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- Create QR codes from any data ([#6090](https://github.com/deltachat/deltachat-core-rust/pull/6090)). -- Add delta chat logo to QR codes ([#6093](https://github.com/deltachat/deltachat-core-rust/pull/6093)). -- Add realtime advertisement received event ([#6043](https://github.com/deltachat/deltachat-core-rust/pull/6043)). -- Notify adding reactions ([#6072](https://github.com/deltachat/deltachat-core-rust/pull/6072)) -- Internal profile names ([#6088](https://github.com/deltachat/deltachat-core-rust/pull/6088)). +- Create QR codes from any data ([#6090](https://github.com/chatmail/core/pull/6090)). +- Add delta chat logo to QR codes ([#6093](https://github.com/chatmail/core/pull/6093)). +- Add realtime advertisement received event ([#6043](https://github.com/chatmail/core/pull/6043)). +- Notify adding reactions ([#6072](https://github.com/chatmail/core/pull/6072)) +- Internal profile names ([#6088](https://github.com/chatmail/core/pull/6088)). ### Features / Changes @@ -651,7 +1210,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes - Replace old draft with a new one atomically. -- ChatId::maybe_delete_draft: Don't delete message if it's not a draft anymore ([#6053](https://github.com/deltachat/deltachat-core-rust/pull/6053)). +- ChatId::maybe_delete_draft: Don't delete message if it's not a draft anymore ([#6053](https://github.com/chatmail/core/pull/6053)). - Call update_connection_history for proxified connections. - sql: Set PRAGMA query_only to avoid writing on read-only connections. - sql: Run `PRAGMA incremental_vacuum` on a write connection. @@ -660,8 +1219,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Build system - Nix flake update. -- Resolve warning about default-features, and make it possible to disable vendoring ([#6079](https://github.com/deltachat/deltachat-core-rust/pull/6079)). -- Silence a rust-analyzer false-positive ([#6077](https://github.com/deltachat/deltachat-core-rust/pull/6077)). +- Resolve warning about default-features, and make it possible to disable vendoring ([#6079](https://github.com/chatmail/core/pull/6079)). +- Silence a rust-analyzer false-positive ([#6077](https://github.com/chatmail/core/pull/6077)). ### CI @@ -687,7 +1246,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - test_qr_setup_contact_svg: Stop testing for no display name. - Always gossip if gossip_period is set to 0. -- test_aeap_flow_verified: Wait for "member added" before sending messages ([#6057](https://github.com/deltachat/deltachat-core-rust/pull/6057)). +- test_aeap_flow_verified: Wait for "member added" before sending messages ([#6057](https://github.com/chatmail/core/pull/6057)). - Make test_verified_group_member_added_recovery more reliable. - test_aeap_flow_verified: Do not start ac1new. - Fix `test_securejoin_after_contact_resetup` flakiness. @@ -710,15 +1269,15 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)). +- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)). - Do not emit progress 1000 when configuration is cancelled. -- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)). -- Re-add tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)). +- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)). +- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)). ### Miscellaneous Tasks - cargo: Bump futures-* from 0.3.30 to 0.3.31. -- cargo: Upgrade async_zip to 0.0.17 ([#6035](https://github.com/deltachat/deltachat-core-rust/pull/6035)). +- cargo: Upgrade async_zip to 0.0.17 ([#6035](https://github.com/chatmail/core/pull/6035)). ### Refactor @@ -734,7 +1293,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Reuse existing connections in background_fetch() if I/O is started. - MsgId::get_info(): Report original filename as well. -- More context for the "Cannot establish guaranteed..." info message ([#6022](https://github.com/deltachat/deltachat-core-rust/pull/6022)). +- More context for the "Cannot establish guaranteed..." info message ([#6022](https://github.com/chatmail/core/pull/6022)). - deltachat-repl: Add `fetch` command to test `background_fetch()`. - deltachat-repl: Print send-backup QR code to the terminal. @@ -762,7 +1321,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Assign message to ad-hoc group with matching name and members ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)). +- Assign message to ad-hoc group with matching name and members ([#5385](https://github.com/chatmail/core/pull/5385)). - Use Rustls instead of native TLS for HTTPS requests. ### Miscellaneous Tasks @@ -803,25 +1362,25 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- [**breaking**] Make QR code type for proxy not specific to SOCKS5 ([#5980](https://github.com/deltachat/deltachat-core-rust/pull/5980)). +- [**breaking**] Make QR code type for proxy not specific to SOCKS5 ([#5980](https://github.com/chatmail/core/pull/5980)). `DC_QR_SOCKS5_PROXY` is replaced with `DC_QR_PROXY`. ### Features / Changes -- Make resending OutPending messages possible ([#5817](https://github.com/deltachat/deltachat-core-rust/pull/5817)). +- Make resending OutPending messages possible ([#5817](https://github.com/chatmail/core/pull/5817)). - Don't SMTP-send messages to self-chat if BccSelf is disabled. - HTTP(S) tunneling. -- Don't put displayname into From/To/Sender if it equals to address ([#5983](https://github.com/deltachat/deltachat-core-rust/pull/5983)). -- Use IMAP APPEND command to upload sync messages ([#5845](https://github.com/deltachat/deltachat-core-rust/pull/5845)). +- Don't put displayname into From/To/Sender if it equals to address ([#5983](https://github.com/chatmail/core/pull/5983)). +- Use IMAP APPEND command to upload sync messages ([#5845](https://github.com/chatmail/core/pull/5845)). - Generate 144-bit group IDs. - smtp: More verbose SMTP connection establishment errors. - Log unexpected message state when resending fails. ### Fixes -- Save QR code token regardless of whether the group exists ([#5954](https://github.com/deltachat/deltachat-core-rust/pull/5954)). -- Shorten message text in locally sent messages too ([#2281](https://github.com/deltachat/deltachat-core-rust/pull/2281)). +- Save QR code token regardless of whether the group exists ([#5954](https://github.com/chatmail/core/pull/5954)). +- Shorten message text in locally sent messages too ([#2281](https://github.com/chatmail/core/pull/2281)). ### Documentation @@ -847,12 +1406,12 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Automatic reconfiguration, e.g. switching to implicit TLS if STARTTLS port stops working. - Always use preloaded DNS results. - Add "Auto-Submitted: auto-replied" header to appropriate SecureJoin messages. -- Parallelize IMAP and SMTP connection attempts ([#5915](https://github.com/deltachat/deltachat-core-rust/pull/5915)). +- Parallelize IMAP and SMTP connection attempts ([#5915](https://github.com/chatmail/core/pull/5915)). - securejoin: Ignore invalid *-request-with-auth messages silently. - ChatId::create_for_contact_with_blocked: Don't emit events on no op. -- Delete messages from a chatmail server immediately by default ([#5805](https://github.com/deltachat/deltachat-core-rust/pull/5805)) ([#5840](https://github.com/deltachat/deltachat-core-rust/pull/5840)). +- Delete messages from a chatmail server immediately by default ([#5805](https://github.com/chatmail/core/pull/5805)) ([#5840](https://github.com/chatmail/core/pull/5840)). - Shadowsocks support. -- Recognize t.me SOCKS5 proxy QR codes ([#5895](https://github.com/deltachat/deltachat-core-rust/pull/5895)) +- Recognize t.me SOCKS5 proxy QR codes ([#5895](https://github.com/chatmail/core/pull/5895)) - Remove old iroh 0.4 and support for old `DCBACKUP` QR codes. ### Fixes @@ -860,7 +1419,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - http: Set I/O timeout to 1 minute rather than whole request timeout. - Add Auto-Submitted header in a single place. - Do not allow quotes with "... wrote:" headers in chat messages. -- Don't sync QR code token before populating the group ([#5935](https://github.com/deltachat/deltachat-core-rust/pull/5935)). +- Don't sync QR code token before populating the group ([#5935](https://github.com/chatmail/core/pull/5935)). ### Documentation @@ -893,25 +1452,25 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Display Config::MdnsEnabled as true by default ([#5948](https://github.com/deltachat/deltachat-core-rust/pull/5948)). +- Display Config::MdnsEnabled as true by default ([#5948](https://github.com/chatmail/core/pull/5948)). ## [1.142.11] - 2024-08-30 ### Fixes -- Set backward verification when observing vc-contact-confirm or `vg-member-added` ([#5930](https://github.com/deltachat/deltachat-core-rust/pull/5930)). +- Set backward verification when observing vc-contact-confirm or `vg-member-added` ([#5930](https://github.com/chatmail/core/pull/5930)). ## [1.142.10] - 2024-08-26 ### Fixes -- Only include one From: header in securejoin messages ([#5917](https://github.com/deltachat/deltachat-core-rust/pull/5917)). +- Only include one From: header in securejoin messages ([#5917](https://github.com/chatmail/core/pull/5917)). ## [1.142.9] - 2024-08-24 ### Fixes -- Fix reading of multiline SMTP greetings ([#5911](https://github.com/deltachat/deltachat-core-rust/pull/5911)). +- Fix reading of multiline SMTP greetings ([#5911](https://github.com/chatmail/core/pull/5911)). ### Features / Changes @@ -928,7 +1487,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes - Do not save "Automatic" into configured_imap_certificate_checks. **This fixes regression introduced in core 1.142.4. Versions 1.142.4..1.142.6 should not be used in releases.** -- Create a group unblocked for bot even if 1:1 chat is blocked ([#5514](https://github.com/deltachat/deltachat-core-rust/pull/5514)). +- Create a group unblocked for bot even if 1:1 chat is blocked ([#5514](https://github.com/chatmail/core/pull/5514)). - Update rpgp from 0.13.1 to 0.13.2 to fix "unable to decrypt" errors when sending messages to old Delta Chat clients and using Ed25519 keys to encrypt. - Do not request ALPN on standard ports and when using STARTTLS. @@ -954,9 +1513,9 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)). -- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)). -- Increase timeout for QR generation to 60s ([#5882](https://github.com/deltachat/deltachat-core-rust/pull/5882)). +- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/chatmail/core/pull/5870)). +- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/chatmail/core/pull/5870)). +- Increase timeout for QR generation to 60s ([#5882](https://github.com/chatmail/core/pull/5882)). ### Documentation @@ -990,7 +1549,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Allow using OAuth 2 with SOCKS5. - Allow autoconfig when SOCKS5 is enabled. - Update provider database. -- cargo: Update iroh from 0.21 to 0.22 ([#5860](https://github.com/deltachat/deltachat-core-rust/pull/5860)). +- cargo: Update iroh from 0.21 to 0.22 ([#5860](https://github.com/chatmail/core/pull/5860)). ### CI @@ -1028,7 +1587,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Try only the full email address if username is unspecified. -- Sort DNS results by successful connection timestamp ([#5818](https://github.com/deltachat/deltachat-core-rust/pull/5818)). +- Sort DNS results by successful connection timestamp ([#5818](https://github.com/chatmail/core/pull/5818)). ### Fixes @@ -1057,14 +1616,14 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Do not reveal sender's language in read receipts ([#5802](https://github.com/deltachat/deltachat-core-rust/pull/5802)). +- Do not reveal sender's language in read receipts ([#5802](https://github.com/chatmail/core/pull/5802)). - Try next DNS resolution result if TLS setup fails. - Report first error instead of the last on connection failure. ### Fixes - smtp: Use DNS cache for implicit TLS connections. -- Imex::import_backup: Unpack all blobs before importing a db ([#4307](https://github.com/deltachat/deltachat-core-rust/pull/4307)). +- Imex::import_backup: Unpack all blobs before importing a db ([#4307](https://github.com/chatmail/core/pull/4307)). - Import_backup_stream: Fix progress stucking at 0. - Sql::import: Detach backup db if any step of the import fails. - Imex::import_backup: Ignore errors from delete_and_reset_all_device_msgs(). @@ -1093,7 +1652,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes - deltachat-jsonrpc: Add `pinned` property to `FullChat` and `BasicChat`. -- deltachat-jsonrpc: Allow to set message quote text without referencing quoted message ([#5695](https://github.com/deltachat/deltachat-core-rust/pull/5695)). +- deltachat-jsonrpc: Allow to set message quote text without referencing quoted message ([#5695](https://github.com/chatmail/core/pull/5695)). ### Features / Changes @@ -1101,25 +1660,25 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - iroh: Pass direct addresses from Endpoint to Gossip. - New BACKUP2 transfer protocol. - Use `[...]` instead of `...` for protected subject. -- Add email address and fingerprint to exported key file names ([#5694](https://github.com/deltachat/deltachat-core-rust/pull/5694)). +- Add email address and fingerprint to exported key file names ([#5694](https://github.com/chatmail/core/pull/5694)). - Request `imap` ALPN for IMAP TLS connections and `smtp` ALPN for SMTP TLS connections. -- Limit the size of aggregated WebXDC update to 100 KiB ([#4825](https://github.com/deltachat/deltachat-core-rust/pull/4825)). -- Don't create ad-hoc group on a member removal message ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)). -- Don't unarchive a group on a member removal except SELF ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)). +- Limit the size of aggregated WebXDC update to 100 KiB ([#4825](https://github.com/chatmail/core/pull/4825)). +- Don't create ad-hoc group on a member removal message ([#5618](https://github.com/chatmail/core/pull/5618)). +- Don't unarchive a group on a member removal except SELF ([#5618](https://github.com/chatmail/core/pull/5618)). - Use custom DNS resolver for HTTP(S). - Promote fallback DNS results to cached on successful use. -- Set summary thumbnail path for WebXDCs to "webxdc-icon://last-msg-id" ([#5782](https://github.com/deltachat/deltachat-core-rust/pull/5782)). +- Set summary thumbnail path for WebXDCs to "webxdc-icon://last-msg-id" ([#5782](https://github.com/chatmail/core/pull/5782)). - Do not show the address in invite QR code SVG. -- Report better error from DcKey::from_asc() ([#5539](https://github.com/deltachat/deltachat-core-rust/pull/5539)). -- Contact::create_ex: Don't send sync message if nothing changed ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)). +- Report better error from DcKey::from_asc() ([#5539](https://github.com/chatmail/core/pull/5539)). +- Contact::create_ex: Don't send sync message if nothing changed ([#5705](https://github.com/chatmail/core/pull/5705)). ### Fixes - `Message::set_quote`: Don't forget to remove `Param::ProtectQuote`. - Randomize avatar blob filenames to work around caching. - Correct copy-pasted DCACCOUNT parsing errors message. -- Call `send_sync_msg()` only from the SMTP loop ([#5780](https://github.com/deltachat/deltachat-core-rust/pull/5780)). -- Emit MsgsChanged if the number of unnoticed archived chats could decrease ([#5768](https://github.com/deltachat/deltachat-core-rust/pull/5768)). +- Call `send_sync_msg()` only from the SMTP loop ([#5780](https://github.com/chatmail/core/pull/5780)). +- Emit MsgsChanged if the number of unnoticed archived chats could decrease ([#5768](https://github.com/chatmail/core/pull/5768)). - Reject message with forged From even if no valid signatures are found. ### Refactor @@ -1149,28 +1708,28 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Add `is_muted` config option. -- Parse vcards exported by protonmail ([#5723](https://github.com/deltachat/deltachat-core-rust/pull/5723)). -- Disable sending sync messages for bots ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)). +- Parse vcards exported by protonmail ([#5723](https://github.com/chatmail/core/pull/5723)). +- Disable sending sync messages for bots ([#5705](https://github.com/chatmail/core/pull/5705)). ### Fixes - Don't fail if going to send plaintext, but some peerstate is missing. -- Correctly sanitize input everywhere ([#5697](https://github.com/deltachat/deltachat-core-rust/pull/5697)). +- Correctly sanitize input everywhere ([#5697](https://github.com/chatmail/core/pull/5697)). - Do not try to register non-iOS tokens for heartbeats. - imap: Reset new_mail if folder is ignored. -- Use and prefer Date from signed message part ([#5716](https://github.com/deltachat/deltachat-core-rust/pull/5716)). +- Use and prefer Date from signed message part ([#5716](https://github.com/chatmail/core/pull/5716)). - Distinguish between database errors and no gossip topic. - MimeFactory::verified: Return true for self-chat. ### Refactor - `MimeFactory::is_e2ee_guaranteed()`: always respect `Param::ForcePlaintext`. -- Protect from reusing migration versions ([#5719](https://github.com/deltachat/deltachat-core-rust/pull/5719)). -- Move `quota_needs_update` calculation to a separate function ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)). +- Protect from reusing migration versions ([#5719](https://github.com/chatmail/core/pull/5719)). +- Move `quota_needs_update` calculation to a separate function ([#5683](https://github.com/chatmail/core/pull/5683)). ### Documentation -- Document vCards in the specification ([#5724](https://github.com/deltachat/deltachat-core-rust/pull/5724)) +- Document vCards in the specification ([#5724](https://github.com/chatmail/core/pull/5724)) ### Miscellaneous Tasks @@ -1192,12 +1751,12 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Update quota if it's stale, not fresh ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)). +- Update quota if it's stale, not fresh ([#5683](https://github.com/chatmail/core/pull/5683)). - sql: Assign migration adding msgs.deleted a new number. ### Refactor -- mimefactory: Factor out header confidentiality policy ([#5715](https://github.com/deltachat/deltachat-core-rust/pull/5715)). +- mimefactory: Factor out header confidentiality policy ([#5715](https://github.com/chatmail/core/pull/5715)). - Improve logging during SMTP/IMAP configuration. ## [1.141.0] - 2024-06-24 @@ -1210,20 +1769,20 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Prefer references to fully downloaded messages for chat assignment ([#5645](https://github.com/deltachat/deltachat-core-rust/pull/5645)). -- Protect From name for verified chats and To names for encrypted chats ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)). +- Prefer references to fully downloaded messages for chat assignment ([#5645](https://github.com/chatmail/core/pull/5645)). +- Protect From name for verified chats and To names for encrypted chats ([#5166](https://github.com/chatmail/core/pull/5166)). - Display vCard contact name in the message summary. -- Case-insensitive search for non-ASCII messages ([#5052](https://github.com/deltachat/deltachat-core-rust/pull/5052)). -- Remove subject prefix from ad-hoc group names ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)). +- Case-insensitive search for non-ASCII messages ([#5052](https://github.com/chatmail/core/pull/5052)). +- Remove subject prefix from ad-hoc group names ([#5385](https://github.com/chatmail/core/pull/5385)). - Replace "Unnamed group" with "👥📧" to avoid translation. -- Sync `Config::MvboxMove` across devices ([#5680](https://github.com/deltachat/deltachat-core-rust/pull/5680)). -- Don't reveal profile data to a not yet verified contact ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)). -- Don't reveal profile data in MDNs ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)). +- Sync `Config::MvboxMove` across devices ([#5680](https://github.com/chatmail/core/pull/5680)). +- Don't reveal profile data to a not yet verified contact ([#5166](https://github.com/chatmail/core/pull/5166)). +- Don't reveal profile data in MDNs ([#5166](https://github.com/chatmail/core/pull/5166)). ### Fixes -- Fetch existing messages for bots as `InFresh` ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)). -- Keep tombstones for two days before deleting ([#3685](https://github.com/deltachat/deltachat-core-rust/pull/3685)). +- Fetch existing messages for bots as `InFresh` ([#4976](https://github.com/chatmail/core/pull/4976)). +- Keep tombstones for two days before deleting ([#3685](https://github.com/chatmail/core/pull/3685)). - Housekeeping: Delete MDNs and webxdc status updates for tombstones. - Delete user-deleted messages on the server even if they show up on IMAP later. - Do not send sync messages if bcc_self is disabled. @@ -1246,15 +1805,15 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Refactor -- Deduplicate dependency versions ([#5691](https://github.com/deltachat/deltachat-core-rust/pull/5691)). +- Deduplicate dependency versions ([#5691](https://github.com/chatmail/core/pull/5691)). - Store public key instead of secret key for peer channels. ### Tests - Image drafted as Viewtype::File is sent as is. -- python: Set delete_server_after=1 ("delete immediately") for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)). +- python: Set delete_server_after=1 ("delete immediately") for bots ([#4976](https://github.com/chatmail/core/pull/4976)). - deltachat-rpc-client: Test that webxdc realtime data is not reordered on the sender. -- python: Wait for bot's DC_EVENT_IMAP_INBOX_IDLE before sending messages to it ([#5699](https://github.com/deltachat/deltachat-core-rust/pull/5699)). +- python: Wait for bot's DC_EVENT_IMAP_INBOX_IDLE before sending messages to it ([#5699](https://github.com/chatmail/core/pull/5699)). ## [1.140.2] - 2024-06-07 @@ -1264,10 +1823,10 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Allow fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)). -- Remove group member locally even if send_msg() fails ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)). -- Revert member addition if the corresponding message couldn't be sent ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)). -- @deltachat/stdio-rpc-server: Make local non-symlinked installation possible by using absolute paths for local dev version ([#5679](https://github.com/deltachat/deltachat-core-rust/pull/5679)). +- Allow fetch_existing_msgs for bots ([#4976](https://github.com/chatmail/core/pull/4976)). +- Remove group member locally even if send_msg() fails ([#5508](https://github.com/chatmail/core/pull/5508)). +- Revert member addition if the corresponding message couldn't be sent ([#5508](https://github.com/chatmail/core/pull/5508)). +- @deltachat/stdio-rpc-server: Make local non-symlinked installation possible by using absolute paths for local dev version ([#5679](https://github.com/chatmail/core/pull/5679)). ### Miscellaneous Tasks @@ -1276,12 +1835,12 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Refactor -- @deltachat/stdio-rpc-server: Use old school require instead of the experimental json import ([#5628](https://github.com/deltachat/deltachat-core-rust/pull/5628)). +- @deltachat/stdio-rpc-server: Use old school require instead of the experimental json import ([#5628](https://github.com/chatmail/core/pull/5628)). ### Tests -- Set fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)). -- Don't leave protected group if some member's key is missing ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)). +- Set fetch_existing_msgs for bots ([#4976](https://github.com/chatmail/core/pull/4976)). +- Don't leave protected group if some member's key is missing ([#5508](https://github.com/chatmail/core/pull/5508)). ## [1.140.1] - 2024-06-05 @@ -1310,15 +1869,15 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)). -- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)). -- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)). -- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)). +- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/chatmail/core/pull/5598)). +- Add config option to enable iroh ([#5607](https://github.com/chatmail/core/pull/5607)). +- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/chatmail/core/pull/5633)). +- Add a db index for reactions by msg_id ([#5507](https://github.com/chatmail/core/pull/5507)). ### Fixes -- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)). -- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)). +- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/chatmail/core/pull/5615)). +- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/chatmail/core/pull/5535)). - Allow creation of groups by outgoing messages without recipients. - Prefer `Chat-Group-ID` over references for new groups. - Do not fail to send images with wrong extensions. @@ -1392,7 +1951,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes - peer_channels: Connect to peers that advertise to you. -- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)). +- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/chatmail/core/pull/5617)). ### Tests @@ -1404,7 +1963,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Scale up contact origins to OutgoingTo when sending a message. -- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)). +- Add import_vcard() ([#5202](https://github.com/chatmail/core/pull/5202)). ### Fixes @@ -1414,7 +1973,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Other -- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)). +- nix: Add nextest ([#5610](https://github.com/chatmail/core/pull/5610)). ## [1.139.3] - 2024-05-20 @@ -1437,7 +1996,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - mimeparser: Take the last header of multiple ones with the same name. - Db migration version 59, it contained an sql syntax error. - Sql syntax error in db migration 27. -- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)). +- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/chatmail/core/pull/5601)). - @deltachat/stdio-rpc-server: set default options for `startDeltaChat`. - Always convert absolute paths to relative in accounts.toml. @@ -1466,22 +2025,22 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Ephemeral peer channels ([#5346](https://github.com/deltachat/deltachat-core-rust/pull/5346)). +- Ephemeral peer channels ([#5346](https://github.com/chatmail/core/pull/5346)). ### Fixes - Save override sender displayname for outgoing messages. - Do not mark the message as seen if it has `location.kml`. -- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/deltachat/deltachat-core-rust/pull/5579)). -- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/deltachat/deltachat-core-rust/pull/5583)). -- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/deltachat/deltachat-core-rust/pull/5588)) -- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/deltachat/deltachat-core-rust/pull/5589)) -- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/deltachat/deltachat-core-rust/pull/5580)) +- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/chatmail/core/pull/5579)). +- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/chatmail/core/pull/5583)). +- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/chatmail/core/pull/5588)) +- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/chatmail/core/pull/5589)) +- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/chatmail/core/pull/5580)) ### API-Changes - deltachat-jsonrpc: Return vcard contact directly in MessageObject. -- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/deltachat/deltachat-core-rust/pull/5584)). +- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/chatmail/core/pull/5584)). - deltachat-rpc-client: Add ViewType.VCARD constant. - deltachat-rpc-client: Add Contact.make_vcard(). - deltachat-rpc-client: Add Chat.send_contact(). @@ -1508,9 +2067,9 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- jsonrpc: Add parse_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)). -- Add Viewtype::Vcard ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)). -- Add make_vcard() ([#5203](https://github.com/deltachat/deltachat-core-rust/pull/5203)). +- jsonrpc: Add parse_vcard() ([#5202](https://github.com/chatmail/core/pull/5202)). +- Add Viewtype::Vcard ([#5202](https://github.com/chatmail/core/pull/5202)). +- Add make_vcard() ([#5203](https://github.com/chatmail/core/pull/5203)). ### Build system @@ -1574,7 +2133,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### CI - Set RUSTUP_WINDOWS_PATH_ADD_BIN to work around `nextest` issue . -- deltachat-rpc-server: Fix upload of npm packages to github releases ([#5564](https://github.com/deltachat/deltachat-core-rust/pull/5564)). +- deltachat-rpc-server: Fix upload of npm packages to github releases ([#5564](https://github.com/chatmail/core/pull/5564)). ### Refactor @@ -1596,7 +2155,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- Add dc_msg_save_file() which saves file copy at the provided path ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)). +- Add dc_msg_save_file() which saves file copy at the provided path ([#4309](https://github.com/chatmail/core/pull/4309)). - Api!(jsonrpc): replace EphemeralTimer tag "variant" with "kind" ### CI @@ -1615,11 +2174,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Delete expired path locations in ephemeral loop. - Delete orphaned POI locations during housekeeping. -- Parsing vCards for contacts sharing ([#5482](https://github.com/deltachat/deltachat-core-rust/pull/5482)). +- Parsing vCards for contacts sharing ([#5482](https://github.com/chatmail/core/pull/5482)). - contact-tools: Support parsing profile images from "PHOTO:data:image/jpeg;base64,...". - contact-tools: Add make_vcard(). - Do not add location markers to messages with non-POI location. -- Make one-to-one chats read-only the first seconds of a SecureJoin ([#5512](https://github.com/deltachat/deltachat-core-rust/pull/5512)). +- Make one-to-one chats read-only the first seconds of a SecureJoin ([#5512](https://github.com/chatmail/core/pull/5512)). ### Fixes @@ -1627,10 +2186,10 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Do not fail to send encrypted quotes to unencrypted chats. - Never prepend subject to message text when bot receives it. - Interrupt location loop when new location is stored. -- Correct message viewtype before recoding image blob ([#5496](https://github.com/deltachat/deltachat-core-rust/pull/5496)). +- Correct message viewtype before recoding image blob ([#5496](https://github.com/chatmail/core/pull/5496)). - Delete POI location when disappearing message expires. - Delete non-POI locations after `delete_device_after`, not immediately. -- Update special chats icons even if they are blocked ([#5509](https://github.com/deltachat/deltachat-core-rust/pull/5509)). +- Update special chats icons even if they are blocked ([#5509](https://github.com/chatmail/core/pull/5509)). - Use ChatIdBlocked::lookup_by_contact() instead of ChatId's method when applicable. ### Miscellaneous Tasks @@ -1662,9 +2221,9 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Merge pull request #5515 from deltachat/dependabot/cargo/quote-1.0.36 - Merge pull request #5522 from deltachat/dependabot/cargo/chrono-0.4.38 - Merge pull request #5523 from deltachat/dependabot/cargo/mailparse-0.15.0 -- Add webxdc internal integration commands in jsonrpc ([#5541](https://github.com/deltachat/deltachat-core-rust/pull/5541)) -- Limit quote replies ([#5543](https://github.com/deltachat/deltachat-core-rust/pull/5543)) -- Stdio jsonrpc server npm package ([#5332](https://github.com/deltachat/deltachat-core-rust/pull/5332)) +- Add webxdc internal integration commands in jsonrpc ([#5541](https://github.com/chatmail/core/pull/5541)) +- Limit quote replies ([#5543](https://github.com/chatmail/core/pull/5543)) +- Stdio jsonrpc server npm package ([#5332](https://github.com/chatmail/core/pull/5332)) ### Refactor @@ -1674,7 +2233,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Tests -- Explain test_was_seen_recently false-positive and give workaround instructions ([#5474](https://github.com/deltachat/deltachat-core-rust/pull/5474)). +- Explain test_was_seen_recently false-positive and give workaround instructions ([#5474](https://github.com/chatmail/core/pull/5474)). - Test that member is added even if "Member added" is lost. - Test that POIs are deleted when ephemeral message expires. - Test ts build on branch @@ -1685,18 +2244,18 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes - [**breaking**] Remove `Stream` implementation for `EventEmitter`. -- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/deltachat/deltachat-core-rust/pull/5461)). +- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/chatmail/core/pull/5461)). ### Features / Changes -- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/deltachat/deltachat-core-rust/pull/5443)). +- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/chatmail/core/pull/5443)). - Replace event channel with broadcast channel. - Mark contact request messages as seen on IMAP. ### Fixes - Convert images to RGB8 (without alpha) before encoding into JPEG to fix sending of large RGBA images. -- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/deltachat/deltachat-core-rust/pull/5445)). +- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/chatmail/core/pull/5445)). - Do not fail if Autocrypt Setup Message has no encryption preference to fix key transfer from K-9 Mail to Delta Chat. - Use only CRLF in Autocrypt Setup Message. - python: Use cached message object if `dc_get_msg()` returns `NULL`. @@ -1729,7 +2288,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - [**breaking**] Remove reactions ffi; all implementations use jsonrpc. - Don't load trashed messages with `Message::load_from_db`. -- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/deltachat/deltachat-core-rust/pull/4476)). +- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/chatmail/core/pull/4476)). - deltachat-rpc-client: Add `check_qr` and `set_config_from_qr` APIs. - deltachat-rpc-client: Add `Account.create_chat()`. - deltachat-rpc-client: Add `Message.wait_until_delivered()`. @@ -1747,10 +2306,10 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Do not emit `MSGS_CHANGED` event for outgoing hidden messages. - `Message::get_summary()` must not return reaction summary. -- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/deltachat/deltachat-core-rust/pull/5377)). +- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/chatmail/core/pull/5377)). - deltachat-jsonrpc: block in `inner_get_backup_qr`. -- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/deltachat/deltachat-core-rust/pull/5366)). -- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/deltachat/deltachat-core-rust/pull/5365)). +- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/chatmail/core/pull/5366)). +- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/chatmail/core/pull/5365)). - Don't try to do `fetch_move_delete()` if Trash is needed but not yet configured. - Assign messages to chats based on not fully downloaded references. - Do not create ad-hoc groups from partial downloads. @@ -1765,8 +2324,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### CI - Use cargo-nextest instead of cargo-test. -- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/deltachat/deltachat-core-rust/pull/5459)). -- Typos in CI files ([#5453](https://github.com/deltachat/deltachat-core-rust/pull/5453)). +- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/chatmail/core/pull/5459)). +- Typos in CI files ([#5453](https://github.com/chatmail/core/pull/5453)). ### Documentation @@ -1777,9 +2336,9 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Do not ignore `Contact::get_by_id` errors in `get_encrinfo`. - deltachat-rpc-client: Use `list`, `set` and `tuple` instead of `typing`. -- Use `clone_from()` ([#5451](https://github.com/deltachat/deltachat-core-rust/pull/5451)). +- Use `clone_from()` ([#5451](https://github.com/chatmail/core/pull/5451)). - Do not check for `is_trash()` in `get_last_reaction_if_newer_than()`. -- Split off functional contact tools into its own crate ([#5444](https://github.com/deltachat/deltachat-core-rust/pull/5444)) +- Split off functional contact tools into its own crate ([#5444](https://github.com/chatmail/core/pull/5444)) - Fix nightly clippy warnings. ### Tests @@ -1797,7 +2356,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)). +- Show reactions in summaries ([#5387](https://github.com/chatmail/core/pull/5387)). ### Tests @@ -1828,7 +2387,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes - [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`. -- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)). +- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/chatmail/core/pull/5384)). - deltachat-rpc-client: Add futures. ### Build system @@ -1837,7 +2396,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - nix: Add outputs for Android binaries. - Add `repository` to Cargo.toml. - python: Remove `setuptools_scm` dependency. -- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)). +- Add development shell ([#5390](https://github.com/chatmail/core/pull/5390)). ### CI @@ -1853,12 +2412,12 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)). +- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/chatmail/core/pull/5362)). - Rescan folders after changing `Config::SentboxWatch`. - Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`. - Put overridden sender name into message info. -- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)). -- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)). +- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/chatmail/core/pull/5354)). +- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/chatmail/core/pull/5419)). ### Refactor @@ -1868,7 +2427,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Tests -- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)). +- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/chatmail/core/pull/5105)). - Remove flaky time check from `test_list_from()`. - Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`) @@ -1921,14 +2480,14 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### 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)) +- Add `save_mime_headers` to debug info ([#5350](https://github.com/chatmail/core/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)). +- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/chatmail/core/pull/5349)). +- Add white background to recoded avatars ([#3787](https://github.com/chatmail/core/pull/3787)). ### Build system @@ -1961,11 +2520,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)). +- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/chatmail/core/pull/5105)). ### Fixes -- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)). +- Remove leading whitespace from Subject ([#5106](https://github.com/chatmail/core/pull/5106)). - Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address. ### Build system @@ -1980,14 +2539,14 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Refactor -- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)). +- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/chatmail/core/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)). +- RPC client: Add missing constants ([#5110](https://github.com/chatmail/core/pull/5110)). ## [1.136.2] - 2024-03-05 @@ -2014,13 +2573,13 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### 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)). +- Recognise Trash folder by name ([#5275](https://github.com/chatmail/core/pull/5275)). +- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/chatmail/core/pull/5253)). +- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/chatmail/core/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)). +- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/chatmail/core/pull/5289)). - imap: Allow `maybe_network` to interrupt connection ratelimit. - imap: Set connectivity to "connecting" only after ratelimit. - Remove `Group-ID` from `Message-ID`. @@ -2037,7 +2596,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - 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)). +- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/chatmail/core/pull/5316)). - Increase MSRV to 1.74.0. ### CI @@ -2064,17 +2623,17 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### 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)). +- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/chatmail/core/pull/5280)). +- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/chatmail/core/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)). +- Fix pytest compat ([#5317](https://github.com/chatmail/core/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)). +- Sync self-avatar across devices ([#4893](https://github.com/chatmail/core/pull/4893)). +- Sync Config::Selfstatus across devices ([#4893](https://github.com/chatmail/core/pull/4893)). - Remove webxdc sending limit. ### Fixes @@ -2086,7 +2645,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### 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)). +- Add a test on protection message sort timestamp ([#5088](https://github.com/chatmail/core/pull/5088)). ### Build system @@ -2114,24 +2673,24 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Add wildcard pattern support to provider database. -- Add device message about outgoing undecryptable messages ([#5164](https://github.com/deltachat/deltachat-core-rust/pull/5164)). -- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/deltachat/deltachat-core-rust/pull/5111)). +- Add device message about outgoing undecryptable messages ([#5164](https://github.com/chatmail/core/pull/5164)). +- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/chatmail/core/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)). +- Basic self-reporting ([#5129](https://github.com/chatmail/core/pull/5129)). ### Fixes -- Dehtml: Don't just truncate text when trying to decode ([#5223](https://github.com/deltachat/deltachat-core-rust/pull/5223)). -- Mark the gossip keys from the message as verified, not the ones from the db ([#5247](https://github.com/deltachat/deltachat-core-rust/pull/5247)). -- Guarantee immediate message deletion if delete_server_after == 0 ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)). -- Never allow a message timestamp to be a lot in the future ([#5249](https://github.com/deltachat/deltachat-core-rust/pull/5249)). +- Dehtml: Don't just truncate text when trying to decode ([#5223](https://github.com/chatmail/core/pull/5223)). +- Mark the gossip keys from the message as verified, not the ones from the db ([#5247](https://github.com/chatmail/core/pull/5247)). +- Guarantee immediate message deletion if delete_server_after == 0 ([#5201](https://github.com/chatmail/core/pull/5201)). +- Never allow a message timestamp to be a lot in the future ([#5249](https://github.com/chatmail/core/pull/5249)). - Imap::configure_mvbox: Do select_with_uidvalidity() before return. - ImapSession::select_or_create_folder(): Don't fail if folder is created in parallel. - Emit ConfigSynced event on the second device. - Create mvbox on setting mvbox_move. - Use SystemTime instead of Instant everywhere. -- Restore database rows removed in previous release; this ensures compatibility when adding second device or importing backup and not all devices run the new core ([#5254](https://github.com/deltachat/deltachat-core-rust/pull/5254)) +- Restore database rows removed in previous release; this ensures compatibility when adding second device or importing backup and not all devices run the new core ([#5254](https://github.com/chatmail/core/pull/5254)) ### Miscellaneous Tasks @@ -2171,28 +2730,28 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Tests -- Delete_server_after="1" should cause immediate message deletion ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)). +- Delete_server_after="1" should cause immediate message deletion ([#5201](https://github.com/chatmail/core/pull/5201)). ## [1.134.0] - 2024-01-31 ### API-Changes -- [**breaking**] JSON-RPC: device message api now requires `Option` instead of `String` for the message ([#5211](https://github.com/deltachat/deltachat-core-rust/pull/5211)). +- [**breaking**] JSON-RPC: device message api now requires `Option` instead of `String` for the message ([#5211](https://github.com/chatmail/core/pull/5211)). - CFFI: add `dc_accounts_background_fetch` and event `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE`. - JSON-RPC: add `accounts_background_fetch`. ### Features / Changes -- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/deltachat/deltachat-core-rust/pull/5217)). +- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/chatmail/core/pull/5217)). - Add support for IMAP METADATA, fetching `/shared/comment` and `/shared/admin` and displaying it in account info. ### Fixes - Add tolerance for macOS and iOS changing `#` to `%23`. - Do not drop unknown report attachments, such as TLS reports. -- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/deltachat/deltachat-core-rust/pull/5213)). +- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/chatmail/core/pull/5213)). - `Chat::resend_msgs`: Guarantee strictly increasing time in the `Date` header. -- Delete resent messages on receiver side ([#5155](https://github.com/deltachat/deltachat-core-rust/pull/5155)). +- Delete resent messages on receiver side ([#5155](https://github.com/chatmail/core/pull/5155)). - Fix iOS build issue. ### CI @@ -2201,32 +2760,32 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Tests -- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/deltachat/deltachat-core-rust/pull/5220)). +- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/chatmail/core/pull/5220)). ## [1.133.2] - 2024-01-24 ### Fixes -- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/deltachat/deltachat-core-rust/issues/5206)) -- No new chats for MDNs with alias ([#5196](https://github.com/deltachat/deltachat-core-rust/issues/5196)) ([#5199](https://github.com/deltachat/deltachat-core-rust/pull/5199)). +- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/chatmail/core/issues/5206)) +- No new chats for MDNs with alias ([#5196](https://github.com/chatmail/core/issues/5196)) ([#5199](https://github.com/chatmail/core/pull/5199)). ## [1.133.1] - 2024-01-21 ### API-Changes -- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/deltachat/deltachat-core-rust/pull/5197)). +- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/chatmail/core/pull/5197)). ### Features / Changes -- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/deltachat/deltachat-core-rust/pull/5195)). +- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/chatmail/core/pull/5195)). ### Fixes - `Chat::send_msg`: Remove encryption-related params from already sent message. This allows to send received encrypted `dc_msg_t` object to unencrypted chat, e.g. in a Python bot. - Set message download state to Failure on IMAP errors. This avoids partially downloaded messages getting stuck in "Downloading..." state without actually being in a download queue. - BCC-to-self even if server deletion is set to "at once". This is a workaround for SMTP servers which do not return response in time, BCC-self works as a confirmation that message was sent out successfully and does not need more retries. -- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/deltachat/deltachat-core-rust/pull/5194)). -- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/deltachat/deltachat-core-rust/pull/3986)). +- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/chatmail/core/pull/5194)). +- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/chatmail/core/pull/3986)). ### Tests @@ -2237,14 +2796,14 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Securejoin protocol implementation refinements - - Track forward and backward verification separately ([#5089](https://github.com/deltachat/deltachat-core-rust/pull/5089)) to avoid inconsistent states. + - Track forward and backward verification separately ([#5089](https://github.com/chatmail/core/pull/5089)) to avoid inconsistent states. - Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol. -- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat. +- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/chatmail/core/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat. - deltachat-repl: Enable INFO logging by default and add timestamps. - Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elements based on the configuration key which is a part of the event. -- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)). -- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)). -- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)). +- Sync contact creation/rename across devices ([#5163](https://github.com/chatmail/core/pull/5163)). +- Encrypt MDNs ([#5175](https://github.com/chatmail/core/pull/5175)). +- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/chatmail/core/pull/5181)). ### Build system @@ -2266,13 +2825,13 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - imap: Limit the rate of LOGIN attempts rather than connection attempts. This is to avoid having to wait for rate limiter right after switching from a bad or offline network to a working network while still guarding against reconnection loop. - Do not ignore `peerstate.save_to_db()` errors. - securejoin: Mark 1:1s as protected regardless of the Config::VerifiedOneOnOneChats. -- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)). +- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/chatmail/core/pull/5115)). - imap: Fail fast on `LIST` errors to avoid busy loop when connection is lost. -- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)). +- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/chatmail/core/pull/5115)). - Do not remove contents from unencrypted [Schleuder](https://schleuder.org/) mailing lists messages. -- Reset message error when scheduling resending ([#5119](https://github.com/deltachat/deltachat-core-rust/pull/5119)). -- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/deltachat/deltachat-core-rust/pull/5101)). -- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)). +- Reset message error when scheduling resending ([#5119](https://github.com/chatmail/core/pull/5119)). +- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/chatmail/core/pull/5101)). +- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/chatmail/core/pull/5088)). ### Refactor @@ -2293,7 +2852,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Add "From:" to protected headers for signed-only messages. -- Sync user actions for ad-hoc groups across devices ([#5065](https://github.com/deltachat/deltachat-core-rust/pull/5065)). +- Sync user actions for ad-hoc groups across devices ([#5065](https://github.com/chatmail/core/pull/5065)). ### Fixes @@ -2317,7 +2876,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Don't sort message creating a protected group over a protection message ([#4963](https://github.com/deltachat/deltachat-core-rust/pull/4963)). +- Don't sort message creating a protected group over a protection message ([#4963](https://github.com/chatmail/core/pull/4963)). - Do not lock accounts.toml on iOS. - Protect groups even if some members are not verified and add `test_securejoin_after_contact_resetup` regression test. @@ -2336,17 +2895,17 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes - Send `Chat-Verified` headers in 1:1 chats. -- Ratelimit IMAP connections ([#4940](https://github.com/deltachat/deltachat-core-rust/pull/4940)). +- Ratelimit IMAP connections ([#4940](https://github.com/chatmail/core/pull/4940)). - Remove receiver limit on `.xdc` size. - Don't affect MimeMessage with "From" and secured headers from encrypted unsigned messages. -- Sync `Config::{MdnsEnabled,ShowEmails}` across devices ([#4954](https://github.com/deltachat/deltachat-core-rust/pull/4954)). -- Sync `Config::Displayname` across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)). +- Sync `Config::{MdnsEnabled,ShowEmails}` across devices ([#4954](https://github.com/chatmail/core/pull/4954)). +- Sync `Config::Displayname` across devices ([#4893](https://github.com/chatmail/core/pull/4893)). - `Chat::rename_ex`: Don't send sync message if usual message is sent. ### Fixes - Lock the database when INSERTing a webxdc update, avoid "Database is locked" errors. -- Use keyring with all private keys when decrypting a message ([#5046](https://github.com/deltachat/deltachat-core-rust/pull/5046)). +- Use keyring with all private keys when decrypting a message ([#5046](https://github.com/chatmail/core/pull/5046)). ### Tests @@ -2364,7 +2923,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes - Allow IMAP servers not returning UIDNEXT on SELECT and STATUS such as mail.163.com. -- Use the correct securejoin strings used in the UI, remove old TODO ([#5047](https://github.com/deltachat/deltachat-core-rust/pull/5047)). +- Use the correct securejoin strings used in the UI, remove old TODO ([#5047](https://github.com/chatmail/core/pull/5047)). - Do not emit events about webxdc update events logged into debug log webxdc. ### Tests @@ -2465,17 +3024,17 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Sync chat contacts across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)). -- Sync creating broadcast lists across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)). -- Sync Chat::name across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)). -- Multi-device broadcast lists ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)). +- Sync chat contacts across devices ([#4953](https://github.com/chatmail/core/pull/4953)). +- Sync creating broadcast lists across devices ([#4953](https://github.com/chatmail/core/pull/4953)). +- Sync Chat::name across devices ([#4953](https://github.com/chatmail/core/pull/4953)). +- Multi-device broadcast lists ([#4953](https://github.com/chatmail/core/pull/4953)). ### Fixes - Encode chat name in the `List-ID` header to avoid SMTPUTF8 errors. - Ignore errors from generating sync messages. -- `Context::execute_sync_items`: Ignore all errors ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)). -- Allow to send unverified securejoin messages to protected chats ([#4982](https://github.com/deltachat/deltachat-core-rust/pull/4982)). +- `Context::execute_sync_items`: Ignore all errors ([#4817](https://github.com/chatmail/core/pull/4817)). +- Allow to send unverified securejoin messages to protected chats ([#4982](https://github.com/chatmail/core/pull/4982)). ## [1.130.0] - 2023-11-10 @@ -2490,7 +3049,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Add secondary verified key. - Add info messages about implicitly added members. - Treat reset state as encryption not preferred. -- Grow sleep durations on errors in Imap::fake_idle() ([#4424](https://github.com/deltachat/deltachat-core-rust/pull/4424)). +- Grow sleep durations on errors in Imap::fake_idle() ([#4424](https://github.com/chatmail/core/pull/4424)). ### Fixes @@ -2500,7 +3059,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Do not apply group changes to special chats. Avoid adding members to the trash chat. - imap: make `UidGrouper` robust against duplicate UIDs. - Do not return hidden chat from `dc_get_chat_id_by_contact_id`. -- Smtp_loop(): Don't grow timeout if interrupted early ([#4833](https://github.com/deltachat/deltachat-core-rust/pull/4833)). +- Smtp_loop(): Don't grow timeout if interrupted early ([#4833](https://github.com/chatmail/core/pull/4833)). ### Refactor @@ -2510,11 +3069,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Tests - Remove unused `--liveconfig` option. -- Test chatlist can load for corrupted chats ([#4979](https://github.com/deltachat/deltachat-core-rust/pull/4979)). +- Test chatlist can load for corrupted chats ([#4979](https://github.com/chatmail/core/pull/4979)). ### Miscellaneous Tasks -- Update provider-db ([#4949](https://github.com/deltachat/deltachat-core-rust/pull/4949)). +- Update provider-db ([#4949](https://github.com/chatmail/core/pull/4949)). ## [1.129.1] - 2023-11-06 @@ -2522,8 +3081,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Update tokio-imap to fix Outlook STATUS parsing bug. - deltachat-rpc-client: Add the Lock around request ID. -- `apply_group_changes`: Don't implicitly delete members locally, add absent ones instead ([#4934](https://github.com/deltachat/deltachat-core-rust/pull/4934)). -- Partial messages do not change group state ([#4900](https://github.com/deltachat/deltachat-core-rust/pull/4900)). +- `apply_group_changes`: Don't implicitly delete members locally, add absent ones instead ([#4934](https://github.com/chatmail/core/pull/4934)). +- Partial messages do not change group state ([#4900](https://github.com/chatmail/core/pull/4900)). ### Tests @@ -2533,31 +3092,31 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- Add JSON-RPC `get_chat_id_by_contact_id` API ([#4918](https://github.com/deltachat/deltachat-core-rust/pull/4918)). +- Add JSON-RPC `get_chat_id_by_contact_id` API ([#4918](https://github.com/chatmail/core/pull/4918)). - [**breaking**] Remove deprecated `get_verifier_addr`. ### Features / Changes -- Sync chat `Blocked` state, chat visibility, chat mute duration and contact blocked status across devices ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)). -- Add 'group created instructions' as info message ([#4916](https://github.com/deltachat/deltachat-core-rust/pull/4916)). +- Sync chat `Blocked` state, chat visibility, chat mute duration and contact blocked status across devices ([#4817](https://github.com/chatmail/core/pull/4817)). +- Add 'group created instructions' as info message ([#4916](https://github.com/chatmail/core/pull/4916)). - Add hardcoded fallback DNS cache. ### Fixes -- Switch to `EncryptionPreference::Mutual` on a receipt of encrypted+signed message ([#4707](https://github.com/deltachat/deltachat-core-rust/pull/4707)). +- Switch to `EncryptionPreference::Mutual` on a receipt of encrypted+signed message ([#4707](https://github.com/chatmail/core/pull/4707)). - imap: Check UIDNEXT with a STATUS command before going IDLE. - Allow to change verified key via "member added" message. - json-rpc: Return verifier even if the contact is not "verified" (Autocrypt key does not equal Secure-Join key). ### Documentation -- Refine `Contact::get_verifier_id` and `Contact::is_verified` documentation ([#4922](https://github.com/deltachat/deltachat-core-rust/pull/4922)). +- Refine `Contact::get_verifier_id` and `Contact::is_verified` documentation ([#4922](https://github.com/chatmail/core/pull/4922)). - Contact profile view should not use `dc_contact_is_verified()`. - Remove documentation for non-existing `dc_accounts_new` `os_name` param. ### Refactor -- Remove unused or useless code paths in Secure-Join ([#4897](https://github.com/deltachat/deltachat-core-rust/pull/4897)). +- Remove unused or useless code paths in Secure-Join ([#4897](https://github.com/chatmail/core/pull/4897)). - Improve error handling in Secure-Join code. - Add hostname to "no DNS resolution results" error message. - Accept `&str` instead of `Option` in idle(). @@ -2565,7 +3124,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ## [1.128.0] - 2023-11-02 ### Build system -- [**breaking**] Upgrade nodejs version to 18 ([#4903](https://github.com/deltachat/deltachat-core-rust/pull/4903)). +- [**breaking**] Upgrade nodejs version to 18 ([#4903](https://github.com/chatmail/core/pull/4903)). ### Features / Changes @@ -2574,7 +3133,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Do not fail securejoin due to unrelated pending bobstate ([#4896](https://github.com/deltachat/deltachat-core-rust/pull/4896)). +- Do not fail securejoin due to unrelated pending bobstate ([#4896](https://github.com/chatmail/core/pull/4896)). - Allow other verified group recipients to be unverified, only check the sender verification. - Remove not working attempt to recover from verified key changes. @@ -2605,15 +3164,15 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - [**breaking**] `dc_accounts_new` API is changed. Unused `os_name` argument is removed and `writable` argument is added. - jsonrpc: Add `resend_messages`. -- [**breaking**] Remove unused function `is_verified_ex()` ([#4551](https://github.com/deltachat/deltachat-core-rust/pull/4551)) +- [**breaking**] Remove unused function `is_verified_ex()` ([#4551](https://github.com/chatmail/core/pull/4551)) - [**breaking**] Make `MsgId.delete_from_db()` private. - [**breaking**] deltachat-jsonrpc: use `kind` as a tag for all union types -- json-rpc: Force stickers to be sent as stickers ([#4819](https://github.com/deltachat/deltachat-core-rust/pull/4819)). -- Add mailto parse api ([#4829](https://github.com/deltachat/deltachat-core-rust/pull/4829)). +- json-rpc: Force stickers to be sent as stickers ([#4819](https://github.com/chatmail/core/pull/4819)). +- Add mailto parse api ([#4829](https://github.com/chatmail/core/pull/4829)). - [**breaking**] Remove unused `DC_STR_PROTECTION_(EN)ABLED` strings - [**breaking**] Remove unused `dc_set_chat_protection()` - Hide `DcSecretKey` trait from the API. -- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config. +- Verified 1:1 chats ([#4315](https://github.com/chatmail/core/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config. - Add api `chat::Chat::is_protection_broken` - Add `dc_chat_is_protection_broken()` C API. @@ -2631,24 +3190,24 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Features / Changes -- Add lockfile to account manager ([#4314](https://github.com/deltachat/deltachat-core-rust/pull/4314)). -- Don't show a contact as verified if their key changed since the verification ([#4574](https://github.com/deltachat/deltachat-core-rust/pull/4574)). +- Add lockfile to account manager ([#4314](https://github.com/chatmail/core/pull/4314)). +- Don't show a contact as verified if their key changed since the verification ([#4574](https://github.com/chatmail/core/pull/4574)). - deltachat-rpc-server: Add `--openrpc` option to print OpenRPC specification for JSON-RPC API. This specification can be used to generate JSON-RPC API clients. -- Track whether contact is a bot or not ([#4821](https://github.com/deltachat/deltachat-core-rust/pull/4821)). -- Replace `Config::SendSyncMsgs` with `SyncMsgs` ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)). +- Track whether contact is a bot or not ([#4821](https://github.com/chatmail/core/pull/4821)). +- Replace `Config::SendSyncMsgs` with `SyncMsgs` ([#4817](https://github.com/chatmail/core/pull/4817)). ### Fixes -- Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt ([#4538](https://github.com/deltachat/deltachat-core-rust/pull/4538)). -- Allow to save a draft if the verification is broken ([#4542](https://github.com/deltachat/deltachat-core-rust/pull/4542)). -- Fix info-message orderings of verified 1:1 chats ([#4545](https://github.com/deltachat/deltachat-core-rust/pull/4545)). +- Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt ([#4538](https://github.com/chatmail/core/pull/4538)). +- Allow to save a draft if the verification is broken ([#4542](https://github.com/chatmail/core/pull/4542)). +- Fix info-message orderings of verified 1:1 chats ([#4545](https://github.com/chatmail/core/pull/4545)). - Fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate -- `receive_imf`: Update peerstate from db after handling Securejoin handshake ([#4600](https://github.com/deltachat/deltachat-core-rust/pull/4600)). -- Sort old incoming messages below all outgoing ones ([#4621](https://github.com/deltachat/deltachat-core-rust/pull/4621)). +- `receive_imf`: Update peerstate from db after handling Securejoin handshake ([#4600](https://github.com/chatmail/core/pull/4600)). +- Sort old incoming messages below all outgoing ones ([#4621](https://github.com/chatmail/core/pull/4621)). - Do not mark non-verified group chats as verified when using securejoin. -- `receive_imf`: Set protection only for Chattype::Single ([#4597](https://github.com/deltachat/deltachat-core-rust/pull/4597)). -- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)). -- Clear VerifiedOneOnOneChats config on backup ([#4615](https://github.com/deltachat/deltachat-core-rust/pull/4615)). +- `receive_imf`: Set protection only for Chattype::Single ([#4597](https://github.com/chatmail/core/pull/4597)). +- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/chatmail/core/pull/4616)). +- Clear VerifiedOneOnOneChats config on backup ([#4615](https://github.com/chatmail/core/pull/4615)). - Try removal of accounts multiple times with timeouts in case the database file is blocked (restore `try_many_times` workaround). ### Build system @@ -2686,14 +3245,14 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### API-Changes -- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)). -- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)). +- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/chatmail/core/pull/4824)). +- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/chatmail/core/pull/4839)). ### Features / Changes -- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)). +- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/chatmail/core/pull/4644)). - This means that UIs need to ask for the name when creating a broadcast list, similar to . -- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820)) +- Add self-address to backup filename ([#4820](https://github.com/chatmail/core/pull/4820)) ### CI @@ -2728,12 +3287,12 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`. - python: Don't automatically set the displayname to "bot" when setting log level. -- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)). -- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)). -- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)). +- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/chatmail/core/pull/4700)). +- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/chatmail/core/pull/4757)). +- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/chatmail/core/pull/4811)). - Set connectivity status to "connected" during fake idle. - Return verifier contacts regardless of their origin. -- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)). +- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/chatmail/core/pull/4534)). ### Refactor @@ -2748,11 +3307,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)). +- Remove footer from reactions on the receiver side ([#4780](https://github.com/chatmail/core/pull/4780)). ### CI -- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788)) +- Pin `urllib3` version to `<2`. ([#4788](https://github.com/chatmail/core/issues/4788)) ## [1.124.0] - 2023-10-04 @@ -2768,19 +3327,19 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Wrap base64-encoded parts to 76 characters. - Require valid email addresses in `dc_provider_new_from_email[_with_dns]`. -- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)). +- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/chatmail/core/issues/4749)). - Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch. - Do not put the status footer into reaction MIME parts. -- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)). -- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)). +- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/chatmail/core/issues/4756)). +- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/chatmail/core/pull/4765)). - Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate. - deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call. -- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages. +- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/chatmail/core/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages. - webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`. ### Features / Changes -- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)). +- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/chatmail/core/issues/3782)). - deltachat-rpc-client: Log exceptions when long-running tasks die. ### Build @@ -2796,8 +3355,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). -- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). +- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/chatmail/core/pull/4676)). +- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/chatmail/core/pull/4676)). - imex: Use "default" in the filename of the default key. ### Miscellaneous Tasks @@ -2814,8 +3373,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. - Reopen all connections on database passpharse change. - Do not block new group chats if 1:1 chat is blocked. -- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). -- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)). +- Improve group membership consistency algorithm ([#3782](https://github.com/chatmail/core/pull/3782))([#4624](https://github.com/chatmail/core/pull/4624)). +- Forbid membership changes from possible non-members ([#3782](https://github.com/chatmail/core/pull/3782)). - `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages. ### Build system @@ -2847,9 +3406,9 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes -- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)). -- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)). -- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)). +- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/chatmail/core/pull/4624)). +- Save mime headers for messages not signed with a known key ([#4557](https://github.com/chatmail/core/pull/4557)). +- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/chatmail/core/pull/4616)). - Do not allow dots at the end of email addresses. - deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies. `aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98). @@ -2863,8 +3422,8 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ### Fixes - Update async-imap to 0.9.1 to fix memory leak. -- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)). -- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)). +- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/chatmail/core/pull/4579)). +- Do not send images without transparency as stickers ([#4611](https://github.com/chatmail/core/pull/4611)). - `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded. ### Refactor @@ -2889,11 +3448,11 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91.. ## [1.119.1] - 2023-08-06 -Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610). +Bugfix release attempting to fix the [iOS build error](https://github.com/chatmail/core/issues/4610). ### Features / Changes -- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)). +- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/chatmail/core/pull/4378)). ### Fixes @@ -2922,7 +3481,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac ### Features / Changes - deltachat-rpc-client: Add `MSG_DELETED` constant. -- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)). +- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/chatmail/core/pull/4309)). ### API-Changes @@ -2958,11 +3517,11 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac ### Fixes - Use different member added/removal messages locally and on the network. -- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)). +- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/chatmail/core/issues/4414)). - Make SVG avatar image work on more platforms (use `xlink:href`). - Preserve indentation when converting plaintext to HTML. - Do not run simplify() on dehtml() output. -- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)). +- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/chatmail/core/pull/4529)). ### Documentation @@ -3033,7 +3592,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac - python: make `Contact.is_verified()` return bool. -- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)). +- rust: add API endpoint `get_status_update` ([#4468](https://github.com/chatmail/core/pull/4468)). - rust: make `WebxdcManifest` type public. @@ -3091,7 +3650,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac - python: Make `test_mdn_asymmetric` less flaky. - Make `test_group_with_removed_message_id` less flaky. -- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)). +- Add golden tests infrastructure ([#4395](https://github.com/chatmail/core/pull/4395)). ### Build system @@ -3126,7 +3685,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac ### JSON-RPC API Changes -- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)). +- Sort reactions in descending order ([#4388](https://github.com/chatmail/core/pull/4388)). - Add API to get reactions outside the message snapshot. - `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`. - `get_chatlist_entries` now returns `Vec` of chatids instead of `ChatListEntries`. @@ -3147,19 +3706,19 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac ### Features / Changes - Build deltachat-rpc-server releases for x86\_64 macOS. -- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)). +- Generate changelogs using git-cliff ([#4393](https://github.com/chatmail/core/pull/4393), [#4396](https://github.com/chatmail/core/pull/4396)). - Improve SMTP logging. - Do not cut incoming text if "bot" config is set. ### Fixes -- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)). -- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)). -- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)). -- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)). +- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/chatmail/core/pull/4373)). +- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/chatmail/core/pull/4383)). +- Don't put a double dot at the end of error messages ([#4398](https://github.com/chatmail/core/pull/4398)). +- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)). - Do not return an error from `send_msg_to_smtp` if retry limit is exceeded. -- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)). -- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)). +- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)). +- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)). ### Refactor @@ -5451,7 +6010,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac - historic: we now use the mailparse crate and lettre-email to generate mime messages. This got rid of mmime completely, the C2rust generated port of the libetpan mime-parse -- IOW 22KLocs of cumbersome code removed! see - https://github.com/deltachat/deltachat-core-rust/pull/904#issuecomment-561163330 + https://github.com/chatmail/core/pull/904#issuecomment-561163330 many thanks @dignifiedquire for making everybody's life easier and @jonhoo (from rust-imap fame) for suggesting to use the mailparse crate :) @@ -5591,137 +6150,156 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/deltac For a full list of changes, please see our closed Pull Requests: -https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed - -[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0 -[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0 -[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1 -[1.112.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.1...v1.112.2 -[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3 -[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4 -[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5 -[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6 -[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7 -[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8 -[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9 -[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10 -[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0 -[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0 -[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0 -[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0 -[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0 -[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0 -[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0 -[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1 -[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0 -[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0 -[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0 -[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0 -[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0 -[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1 -[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0 -[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0 -[1.126.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.0...v1.126.1 -[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0 -[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1 -[1.127.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.1...v1.127.2 -[1.128.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.2...v1.128.0 -[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0 -[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1 -[1.130.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.1...v1.130.0 -[1.131.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.130.0...v1.131.0 -[1.131.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.0...v1.131.1 -[1.131.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.1...v1.131.2 -[1.131.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.2...v1.131.3 -[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4 -[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5 -[1.131.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.5...v1.131.6 -[1.131.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.6...v1.131.7 -[1.131.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.7...v1.131.8 -[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9 -[1.132.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.9...v1.132.0 -[1.132.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.0...v1.132.1 -[1.133.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.1...v1.133.0 -[1.133.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.0...v1.133.1 -[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2 -[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0 -[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0 -[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1 -[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0 -[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1 -[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2 -[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3 -[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4 -[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5 -[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6 -[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0 -[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1 -[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2 -[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3 -[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4 -[1.138.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.4...v1.138.0 -[1.138.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.0...v1.138.1 -[1.138.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.1...v1.138.2 -[1.138.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.2...v1.138.3 -[1.138.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.3...v1.138.4 -[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5 -[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0 -[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1 -[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2 -[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3 -[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4 -[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5 -[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6 -[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0 -[1.140.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.0...v1.140.1 -[1.140.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.1...v1.140.2 -[1.141.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.2...v1.141.0 -[1.141.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.0...v1.141.1 -[1.141.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.1...v1.141.2 -[1.142.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.2...v1.142.0 -[1.142.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.0...v1.142.1 -[1.142.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.1...v1.142.2 -[1.142.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.2...v1.142.3 -[1.142.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.3...v1.142.4 -[1.142.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.4...v1.142.5 -[1.142.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.5...v1.142.6 -[1.142.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.6...v1.142.7 -[1.142.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.7...v1.142.8 -[1.142.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.8...v1.142.9 -[1.142.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.9..v1.142.10 -[1.142.11]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.10..v1.142.11 -[1.142.12]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.11..v1.142.12 -[1.143.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.12..v1.143.0 -[1.144.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.143.0..v1.144.0 -[1.145.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.144.0..v1.145.0 -[1.146.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.145.0..v1.146.0 -[1.147.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.146.0..v1.147.0 -[1.147.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.0..v1.147.1 -[1.148.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.1..v1.148.0 -[1.148.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.0..v1.148.1 -[1.148.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.1..v1.148.2 -[1.148.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.2..v1.148.3 -[1.148.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.3..v1.148.4 -[1.148.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.4..v1.148.5 -[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6 -[1.148.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.6..v1.148.7 -[1.149.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.7..v1.149.0 -[1.150.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.149.0..v1.150.0 -[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0 -[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1 -[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2 -[1.151.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.2..v1.151.3 -[1.151.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.3..v1.151.4 -[1.151.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.4..v1.151.5 -[1.151.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.5..v1.151.6 -[1.152.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.6..v1.152.0 -[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1 -[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2 -[1.153.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.2..v1.153.0 -[1.154.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.153.0..v1.154.0 -[1.154.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.0..v1.154.1 -[1.154.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.1..v1.154.2 -[1.154.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.2..v1.154.3 -[1.155.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.3..v1.155.0 -[1.155.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.0..v1.155.1 -[1.155.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.1..v1.155.2 +https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed + +[1.111.0]: https://github.com/chatmail/core/compare/v1.110.0...v1.111.0 +[1.112.0]: https://github.com/chatmail/core/compare/v1.111.0...v1.112.0 +[1.112.1]: https://github.com/chatmail/core/compare/v1.112.0...v1.112.1 +[1.112.2]: https://github.com/chatmail/core/compare/v1.112.1...v1.112.2 +[1.112.3]: https://github.com/chatmail/core/compare/v1.112.2...v1.112.3 +[1.112.4]: https://github.com/chatmail/core/compare/v1.112.3...v1.112.4 +[1.112.5]: https://github.com/chatmail/core/compare/v1.112.4...v1.112.5 +[1.112.6]: https://github.com/chatmail/core/compare/v1.112.5...v1.112.6 +[1.112.7]: https://github.com/chatmail/core/compare/v1.112.6...v1.112.7 +[1.112.8]: https://github.com/chatmail/core/compare/v1.112.7...v1.112.8 +[1.112.9]: https://github.com/chatmail/core/compare/v1.112.8...v1.112.9 +[1.112.10]: https://github.com/chatmail/core/compare/v1.112.9...v1.112.10 +[1.113.0]: https://github.com/chatmail/core/compare/v1.112.9...v1.113.0 +[1.114.0]: https://github.com/chatmail/core/compare/v1.113.0...v1.114.0 +[1.115.0]: https://github.com/chatmail/core/compare/v1.114.0...v1.115.0 +[1.116.0]: https://github.com/chatmail/core/compare/v1.115.0...v1.116.0 +[1.117.0]: https://github.com/chatmail/core/compare/v1.116.0...v1.117.0 +[1.118.0]: https://github.com/chatmail/core/compare/v1.117.0...v1.118.0 +[1.119.0]: https://github.com/chatmail/core/compare/v1.118.0...v1.119.0 +[1.119.1]: https://github.com/chatmail/core/compare/v1.119.0...v1.119.1 +[1.120.0]: https://github.com/chatmail/core/compare/v1.119.1...v1.120.0 +[1.121.0]: https://github.com/chatmail/core/compare/v1.120.0...v1.121.0 +[1.122.0]: https://github.com/chatmail/core/compare/v1.121.0...v1.122.0 +[1.123.0]: https://github.com/chatmail/core/compare/v1.122.0...v1.123.0 +[1.124.0]: https://github.com/chatmail/core/compare/v1.123.0...v1.124.0 +[1.124.1]: https://github.com/chatmail/core/compare/v1.124.0...v1.124.1 +[1.125.0]: https://github.com/chatmail/core/compare/v1.124.1...v1.125.0 +[1.126.0]: https://github.com/chatmail/core/compare/v1.125.0...v1.126.0 +[1.126.1]: https://github.com/chatmail/core/compare/v1.126.0...v1.126.1 +[1.127.0]: https://github.com/chatmail/core/compare/v1.126.1...v1.127.0 +[1.127.1]: https://github.com/chatmail/core/compare/v1.127.0...v1.127.1 +[1.127.2]: https://github.com/chatmail/core/compare/v1.127.1...v1.127.2 +[1.128.0]: https://github.com/chatmail/core/compare/v1.127.2...v1.128.0 +[1.129.0]: https://github.com/chatmail/core/compare/v1.128.0...v1.129.0 +[1.129.1]: https://github.com/chatmail/core/compare/v1.129.0...v1.129.1 +[1.130.0]: https://github.com/chatmail/core/compare/v1.129.1...v1.130.0 +[1.131.0]: https://github.com/chatmail/core/compare/v1.130.0...v1.131.0 +[1.131.1]: https://github.com/chatmail/core/compare/v1.131.0...v1.131.1 +[1.131.2]: https://github.com/chatmail/core/compare/v1.131.1...v1.131.2 +[1.131.3]: https://github.com/chatmail/core/compare/v1.131.2...v1.131.3 +[1.131.4]: https://github.com/chatmail/core/compare/v1.131.3...v1.131.4 +[1.131.5]: https://github.com/chatmail/core/compare/v1.131.4...v1.131.5 +[1.131.6]: https://github.com/chatmail/core/compare/v1.131.5...v1.131.6 +[1.131.7]: https://github.com/chatmail/core/compare/v1.131.6...v1.131.7 +[1.131.8]: https://github.com/chatmail/core/compare/v1.131.7...v1.131.8 +[1.131.9]: https://github.com/chatmail/core/compare/v1.131.8...v1.131.9 +[1.132.0]: https://github.com/chatmail/core/compare/v1.131.9...v1.132.0 +[1.132.1]: https://github.com/chatmail/core/compare/v1.132.0...v1.132.1 +[1.133.0]: https://github.com/chatmail/core/compare/v1.132.1...v1.133.0 +[1.133.1]: https://github.com/chatmail/core/compare/v1.133.0...v1.133.1 +[1.133.2]: https://github.com/chatmail/core/compare/v1.133.1...v1.133.2 +[1.134.0]: https://github.com/chatmail/core/compare/v1.133.2...v1.134.0 +[1.135.0]: https://github.com/chatmail/core/compare/v1.134.0...v1.135.0 +[1.135.1]: https://github.com/chatmail/core/compare/v1.135.0...v1.135.1 +[1.136.0]: https://github.com/chatmail/core/compare/v1.135.1...v1.136.0 +[1.136.1]: https://github.com/chatmail/core/compare/v1.136.0...v1.136.1 +[1.136.2]: https://github.com/chatmail/core/compare/v1.136.1...v1.136.2 +[1.136.3]: https://github.com/chatmail/core/compare/v1.136.2...v1.136.3 +[1.136.4]: https://github.com/chatmail/core/compare/v1.136.3...v1.136.4 +[1.136.5]: https://github.com/chatmail/core/compare/v1.136.4...v1.136.5 +[1.136.6]: https://github.com/chatmail/core/compare/v1.136.5...v1.136.6 +[1.137.0]: https://github.com/chatmail/core/compare/v1.136.6...v1.137.0 +[1.137.1]: https://github.com/chatmail/core/compare/v1.137.0...v1.137.1 +[1.137.2]: https://github.com/chatmail/core/compare/v1.137.1...v1.137.2 +[1.137.3]: https://github.com/chatmail/core/compare/v1.137.2...v1.137.3 +[1.137.4]: https://github.com/chatmail/core/compare/v1.137.3...v1.137.4 +[1.138.0]: https://github.com/chatmail/core/compare/v1.137.4...v1.138.0 +[1.138.1]: https://github.com/chatmail/core/compare/v1.138.0...v1.138.1 +[1.138.2]: https://github.com/chatmail/core/compare/v1.138.1...v1.138.2 +[1.138.3]: https://github.com/chatmail/core/compare/v1.138.2...v1.138.3 +[1.138.4]: https://github.com/chatmail/core/compare/v1.138.3...v1.138.4 +[1.138.5]: https://github.com/chatmail/core/compare/v1.138.4...v1.138.5 +[1.139.0]: https://github.com/chatmail/core/compare/v1.138.5...v1.139.0 +[1.139.1]: https://github.com/chatmail/core/compare/v1.139.0...v1.139.1 +[1.139.2]: https://github.com/chatmail/core/compare/v1.139.1...v1.139.2 +[1.139.3]: https://github.com/chatmail/core/compare/v1.139.2...v1.139.3 +[1.139.4]: https://github.com/chatmail/core/compare/v1.139.3...v1.139.4 +[1.139.5]: https://github.com/chatmail/core/compare/v1.139.4...v1.139.5 +[1.139.6]: https://github.com/chatmail/core/compare/v1.139.5...v1.139.6 +[1.140.0]: https://github.com/chatmail/core/compare/v1.139.6...v1.140.0 +[1.140.1]: https://github.com/chatmail/core/compare/v1.140.0...v1.140.1 +[1.140.2]: https://github.com/chatmail/core/compare/v1.140.1...v1.140.2 +[1.141.0]: https://github.com/chatmail/core/compare/v1.140.2...v1.141.0 +[1.141.1]: https://github.com/chatmail/core/compare/v1.141.0...v1.141.1 +[1.141.2]: https://github.com/chatmail/core/compare/v1.141.1...v1.141.2 +[1.142.0]: https://github.com/chatmail/core/compare/v1.141.2...v1.142.0 +[1.142.1]: https://github.com/chatmail/core/compare/v1.142.0...v1.142.1 +[1.142.2]: https://github.com/chatmail/core/compare/v1.142.1...v1.142.2 +[1.142.3]: https://github.com/chatmail/core/compare/v1.142.2...v1.142.3 +[1.142.4]: https://github.com/chatmail/core/compare/v1.142.3...v1.142.4 +[1.142.5]: https://github.com/chatmail/core/compare/v1.142.4...v1.142.5 +[1.142.6]: https://github.com/chatmail/core/compare/v1.142.5...v1.142.6 +[1.142.7]: https://github.com/chatmail/core/compare/v1.142.6...v1.142.7 +[1.142.8]: https://github.com/chatmail/core/compare/v1.142.7...v1.142.8 +[1.142.9]: https://github.com/chatmail/core/compare/v1.142.8...v1.142.9 +[1.142.10]: https://github.com/chatmail/core/compare/v1.142.9..v1.142.10 +[1.142.11]: https://github.com/chatmail/core/compare/v1.142.10..v1.142.11 +[1.142.12]: https://github.com/chatmail/core/compare/v1.142.11..v1.142.12 +[1.143.0]: https://github.com/chatmail/core/compare/v1.142.12..v1.143.0 +[1.144.0]: https://github.com/chatmail/core/compare/v1.143.0..v1.144.0 +[1.145.0]: https://github.com/chatmail/core/compare/v1.144.0..v1.145.0 +[1.146.0]: https://github.com/chatmail/core/compare/v1.145.0..v1.146.0 +[1.147.0]: https://github.com/chatmail/core/compare/v1.146.0..v1.147.0 +[1.147.1]: https://github.com/chatmail/core/compare/v1.147.0..v1.147.1 +[1.148.0]: https://github.com/chatmail/core/compare/v1.147.1..v1.148.0 +[1.148.1]: https://github.com/chatmail/core/compare/v1.148.0..v1.148.1 +[1.148.2]: https://github.com/chatmail/core/compare/v1.148.1..v1.148.2 +[1.148.3]: https://github.com/chatmail/core/compare/v1.148.2..v1.148.3 +[1.148.4]: https://github.com/chatmail/core/compare/v1.148.3..v1.148.4 +[1.148.5]: https://github.com/chatmail/core/compare/v1.148.4..v1.148.5 +[1.148.6]: https://github.com/chatmail/core/compare/v1.148.5..v1.148.6 +[1.148.7]: https://github.com/chatmail/core/compare/v1.148.6..v1.148.7 +[1.149.0]: https://github.com/chatmail/core/compare/v1.148.7..v1.149.0 +[1.150.0]: https://github.com/chatmail/core/compare/v1.149.0..v1.150.0 +[1.151.0]: https://github.com/chatmail/core/compare/v1.150.0..v1.151.0 +[1.151.1]: https://github.com/chatmail/core/compare/v1.151.0..v1.151.1 +[1.151.2]: https://github.com/chatmail/core/compare/v1.151.1..v1.151.2 +[1.151.3]: https://github.com/chatmail/core/compare/v1.151.2..v1.151.3 +[1.151.4]: https://github.com/chatmail/core/compare/v1.151.3..v1.151.4 +[1.151.5]: https://github.com/chatmail/core/compare/v1.151.4..v1.151.5 +[1.151.6]: https://github.com/chatmail/core/compare/v1.151.5..v1.151.6 +[1.152.0]: https://github.com/chatmail/core/compare/v1.151.6..v1.152.0 +[1.152.1]: https://github.com/chatmail/core/compare/v1.152.0..v1.152.1 +[1.152.2]: https://github.com/chatmail/core/compare/v1.152.1..v1.152.2 +[1.153.0]: https://github.com/chatmail/core/compare/v1.152.2..v1.153.0 +[1.154.0]: https://github.com/chatmail/core/compare/v1.153.0..v1.154.0 +[1.154.1]: https://github.com/chatmail/core/compare/v1.154.0..v1.154.1 +[1.154.2]: https://github.com/chatmail/core/compare/v1.154.1..v1.154.2 +[1.154.3]: https://github.com/chatmail/core/compare/v1.154.2..v1.154.3 +[1.155.0]: https://github.com/chatmail/core/compare/v1.154.3..v1.155.0 +[1.155.1]: https://github.com/chatmail/core/compare/v1.155.0..v1.155.1 +[1.155.2]: https://github.com/chatmail/core/compare/v1.155.1..v1.155.2 +[1.155.3]: https://github.com/chatmail/core/compare/v1.155.2..v1.155.3 +[1.155.4]: https://github.com/chatmail/core/compare/v1.155.3..v1.155.4 +[1.155.5]: https://github.com/chatmail/core/compare/v1.155.4..v1.155.5 +[1.155.6]: https://github.com/chatmail/core/compare/v1.155.5..v1.155.6 +[1.156.0]: https://github.com/chatmail/core/compare/v1.155.6..v1.156.0 +[1.156.1]: https://github.com/chatmail/core/compare/v1.156.0..v1.156.1 +[1.156.2]: https://github.com/chatmail/core/compare/v1.156.1..v1.156.2 +[1.156.3]: https://github.com/chatmail/core/compare/v1.156.2..v1.156.3 +[1.157.0]: https://github.com/chatmail/core/compare/v1.156.3..v1.157.0 +[1.157.1]: https://github.com/chatmail/core/compare/v1.157.0..v1.157.1 +[1.157.2]: https://github.com/chatmail/core/compare/v1.157.1..v1.157.2 +[1.157.3]: https://github.com/chatmail/core/compare/v1.157.2..v1.157.3 +[1.158.0]: https://github.com/chatmail/core/compare/v1.157.3..v1.158.0 +[1.159.0]: https://github.com/chatmail/core/compare/v1.158.0..v1.159.0 +[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1 +[1.159.2]: https://github.com/chatmail/core/compare/v1.159.1..v1.159.2 +[1.159.3]: https://github.com/chatmail/core/compare/v1.159.2..v1.159.3 +[1.159.4]: https://github.com/chatmail/core/compare/v1.159.3..v1.159.4 +[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28407d1f49..4385f06429 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Bug reports -If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues). +If you found a bug, [report it on GitHub](https://github.com/chatmail/core/issues). If the bug you found is specific to [Android](https://github.com/deltachat/deltachat-android/issues), [iOS](https://github.com/deltachat/deltachat-ios/issues) or @@ -67,7 +67,7 @@ If you want to contribute a code, follow this guide. BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)` ``` -4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls). +4. [**Open a Pull Request**](https://github.com/chatmail/core/pulls). Refer to the corresponding issue. @@ -116,7 +116,7 @@ For other ways to contribute, refer to the [website](https://delta.chat/en/contr You can find the list of good first issues and a link to this guide -on the contributing page: +on the contributing page: [Conventional Commits]: https://www.conventionalcommits.org/ [git-cliff]: https://git-cliff.org/ diff --git a/Cargo.lock b/Cargo.lock index b9c51a020f..f1016bd057 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,21 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aead" @@ -71,7 +71,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.32", ] [[package]] @@ -125,59 +125,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "anstream" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "utf8parse", -] - [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" -[[package]] -name = "anstyle-parse" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" dependencies = [ "backtrace", ] @@ -197,21 +155,15 @@ dependencies = [ [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "ascii_utils" -version = "0.9.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1-rs" @@ -222,11 +174,11 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror 1.0.69", - "time 0.3.36", + "time", ] [[package]] @@ -237,7 +189,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "synstructure", ] @@ -249,7 +201,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -258,7 +210,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.4.0", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -287,11 +239,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-compression" -version = "0.4.15" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26a9844c659a2a293d239c7910b752f8487fe122c6c8bd1659bf85a6507c302" +checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" dependencies = [ "flate2", "futures-core", @@ -303,20 +268,19 @@ dependencies = [ [[package]] name = "async-imap" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5488cd022c3c7bc41a9b34a540d9ac0d9c5cd42fdb106a67616521b7592d5b4e" +checksum = "ca726c61b73c471f531b65e83e161776ba62c2b6ba4ec73d51fad357009ed00a" dependencies = [ "async-channel 2.3.1", "async-compression", - "base64 0.21.7", + "base64", "bytes", "chrono", "futures", "imap-proto", "log", - "nom", - "once_cell", + "nom 7.1.3", "pin-project", "pin-utils", "self_cell", @@ -326,12 +290,14 @@ dependencies = [ ] [[package]] -name = "async-mutex" -version = "1.4.0" +name = "async-lock" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 2.5.3", + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", ] [[package]] @@ -346,42 +312,42 @@ dependencies = [ "url", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "async-smtp" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee04bcf0a7ebf5594f9aff84935dc8cb0490b65055913a7a4c4d08f81e181d6" +checksum = "55219982f938e74491ba85dc4e49cefe8096b1e8f49348c67180a7d244988dca" dependencies = [ "anyhow", - "base64 0.13.1", + "base64", "futures", "log", - "nom", + "nom 8.0.0", "pin-project", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", ] [[package]] @@ -392,7 +358,7 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ "async-compression", "crc32fast", - "futures-lite 2.5.0", + "futures-lite", "pin-project", "thiserror 1.0.69", "tokio", @@ -418,92 +384,34 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "axum" -version = "0.7.5" +name = "backon" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" dependencies = [ - "async-trait", - "axum-core", - "base64 0.21.7", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 1.0.0", + "fastrand", + "gloo-timers", "tokio", - "tokio-tungstenite", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.12", - "instant", - "rand 0.8.5", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -513,28 +421,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" +name = "base32" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -550,9 +440,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitfield" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" [[package]] name = "bitflags" @@ -562,9 +452,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "blake2" @@ -577,9 +467,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.5" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -616,17 +506,107 @@ dependencies = [ "cipher", ] +[[package]] +name = "bolero" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c" +dependencies = [ + "bolero-afl", + "bolero-engine", + "bolero-generator", + "bolero-honggfuzz", + "bolero-kani", + "bolero-libfuzzer", + "cfg-if", + "rand 0.8.5", +] + +[[package]] +name = "bolero-afl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89" +dependencies = [ + "bolero-engine", + "cc", +] + +[[package]] +name = "bolero-engine" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2" +dependencies = [ + "anyhow", + "backtrace", + "bolero-generator", + "lazy_static", + "pretty-hex", + "rand 0.8.5", +] + +[[package]] +name = "bolero-generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb" +dependencies = [ + "bolero-generator-derive", + "either", + "rand_core 0.6.4", +] + +[[package]] +name = "bolero-generator-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bolero-honggfuzz" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-kani" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-libfuzzer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98" +dependencies = [ + "bolero-engine", + "cc", +] + [[package]] name = "bounded-integer" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a6932c88f1d2c29533a3b8a5f5a2f84cc19c3339b431677c3160c5c2e6ca85" +checksum = "102dbef1187b1893e6dfe05a774e79fd52265f49f214f6879c8ff49f52c8188b" [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "cf19e729cdbd51af9a397fb9ef8ac8378007b797f8273cfbfdf45dcaa316167b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -635,9 +615,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -645,9 +625,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "serde", @@ -655,18 +635,18 @@ dependencies = [ [[package]] name = "buffer-redux" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9f8ddd22e0a12391d1e7ada69ec3b0da1914f1cec39c5cf977143c5b2854f5" +checksum = "4e8acf87c5b9f5897cd3ebb9a327f420e0cae9dd4e5c1d2e36f2c84c571a58f1" dependencies = [ "memchr", ] [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byte_string" @@ -676,9 +656,9 @@ checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" [[package]] name = "bytemuck" -version = "1.16.3" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] name = "byteorder" @@ -694,9 +674,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -713,18 +693,18 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -759,19 +739,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.16" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfb-mode" version = "0.8.2" @@ -819,27 +793,25 @@ dependencies = [ [[package]] name = "charset" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" dependencies = [ - "base64 0.13.1", + "base64", "encoding_rs", ] [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -882,18 +854,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.18" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstyle", "clap_lex", @@ -901,15 +873,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9a0700e0127ba15d1d52dd742097f821cd9c65939303a44d970465040a297" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -937,22 +909,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -968,26 +924,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_format" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1022,15 +958,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1058,9 +994,9 @@ checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1103,20 +1039,26 @@ dependencies = [ "itertools", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1133,9 +1075,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -1143,10 +1085,10 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "crossterm_winapi", "parking_lot", - "rustix", + "rustix 0.38.44", "winapi", ] @@ -1161,9 +1103,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1239,7 +1181,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest", - "fiat-crypto 0.2.6", + "fiat-crypto", "rand_core 0.6.4", "rustc_version", "serde", @@ -1255,7 +1197,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1279,7 +1221,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1290,28 +1232,14 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.94", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.101", ] [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbl" @@ -1324,7 +1252,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.155.2" +version = "1.159.5" dependencies = [ "anyhow", "async-broadcast", @@ -1333,7 +1261,7 @@ dependencies = [ "async-native-tls", "async-smtp", "async_zip", - "base64 0.22.1", + "base64", "blake3", "brotli", "bytes", @@ -1343,14 +1271,12 @@ dependencies = [ "deltachat-contact-tools", "deltachat-time", "deltachat_derive", - "email", - "encoded-words", "escaper", "fast-socks5", "fd-lock", "format-flowed", "futures", - "futures-lite 2.5.0", + "futures-lite", "hex", "hickory-resolver", "http-body-util", @@ -1361,16 +1287,15 @@ dependencies = [ "iroh", "iroh-gossip", "kamadak-exif", - "lettre_email", "libc", "log", + "mail-builder", "mailparse", "mime", "nu-ansi-term", "num-derive", "num-traits", "num_cpus", - "once_cell", "parking_lot", "percent-encoding", "pgp", @@ -1395,13 +1320,13 @@ dependencies = [ "sha2", "shadowsocks", "smallvec", - "strum", - "strum_macros", + "strum 0.27.1", + "strum_macros 0.27.1", "tagger", "tempfile", "testdir", "textwrap", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tokio-io-timeout", "tokio-rustls", @@ -1420,24 +1345,30 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", - "once_cell", "regex", "rusqlite", ] +[[package]] +name = "deltachat-fuzz" +version = "0.0.0" +dependencies = [ + "bolero", + "deltachat", + "format-flowed", + "mailparse", +] + [[package]] name = "deltachat-jsonrpc" -version = "1.155.2" +version = "1.159.5" dependencies = [ "anyhow", "async-channel 2.3.1", - "axum", - "base64 0.22.1", + "base64", "deltachat", "deltachat-contact-tools", - "env_logger", "futures", - "log", "num-traits", "sanitize-filename", "schemars", @@ -1452,7 +1383,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.155.2" +version = "1.159.5" dependencies = [ "anyhow", "deltachat", @@ -1468,12 +1399,12 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.155.2" +version = "1.159.5" dependencies = [ "anyhow", "deltachat", "deltachat-jsonrpc", - "futures-lite 2.5.0", + "futures-lite", "log", "serde", "serde_json", @@ -1492,12 +1423,12 @@ name = "deltachat_derive" version = "2.0.0" dependencies = [ "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "deltachat_ffi" -version = "1.155.2" +version = "1.159.5" dependencies = [ "anyhow", "deltachat", @@ -1505,19 +1436,18 @@ dependencies = [ "human-panic", "libc", "num-traits", - "once_cell", "rand 0.8.5", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "yerpc", ] [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "der_derive", @@ -1533,7 +1463,7 @@ checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", - "nom", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -1541,13 +1471,13 @@ dependencies = [ [[package]] name = "der_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1561,33 +1491,33 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1607,7 +1537,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "unicode-xid", ] @@ -1646,23 +1576,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1673,7 +1603,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1689,9 +1619,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" dependencies = [ "litrs", ] @@ -1712,23 +1642,17 @@ dependencies = [ "zeroize", ] -[[package]] -name = "dtoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" - [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" [[package]] name = "dynosaur" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fac44672fabad44990176319b9e94393f3a38b960b5ca2af6cd90f5ecd1497" +checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890" dependencies = [ "dynosaur_derive", "trait-variant", @@ -1736,13 +1660,13 @@ dependencies = [ [[package]] name = "dynosaur_derive" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c187d1e575ef546d24f0fcd7701cc04abfe6b5e7e2758aabc450b99e835ac3" +checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -1798,22 +1722,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", -] - [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" @@ -1836,20 +1749,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "email" -version = "0.0.20" -source = "git+https://github.com/deltachat/rust-email?branch=master#ba176ca31ae000203368eb9baacc7eb469fd7692" -dependencies = [ - "base64 0.11.0", - "chrono", - "encoded-words", - "encoding", - "lazy_static", - "rand 0.7.3", - "version_check", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -1857,89 +1756,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" [[package]] -name = "encoded-words" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1693107e6084e2b9444d34a985697f56c8832d314924d5cfb1fb7793154bef" -dependencies = [ - "base64 0.12.3", - "charset", - "encoding_rs", - "hex", - "lazy_static", - "regex", - "thiserror 1.0.69", -] - -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" +name = "embedded-io" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1962,91 +1788,53 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "enumflags2" -version = "0.7.9" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.9" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "env_filter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", + "syn 2.0.101", ] [[package]] name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - -[[package]] -name = "erased_set" -version = "0.8.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a02a5d186d7bf1cb21f1f95e1a9cfa5c1f2dcd803a47aad454423ceec13525c5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "escaper" @@ -2065,9 +1853,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -2076,11 +1864,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.4.0", "pin-project-lite", ] @@ -2110,46 +1898,28 @@ dependencies = [ "tokio-stream", ] -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "4.0.2" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix", - "windows-sys 0.52.0", + "rustix 1.0.5", + "windows-sys 0.59.0", ] [[package]] name = "fdeflate" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -2166,15 +1936,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "fiat-crypto" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -2196,26 +1960,14 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2267,14 +2019,15 @@ dependencies = [ [[package]] name = "futures-buffered" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34acda8ae8b63fbe0b2195c998b180cff89a8212fb2622a78b572a9f1c6f7684" +checksum = "fe940397c8b744b9c2c974791c2c08bca2c3242ce0290393249e98f215a00472" dependencies = [ "cordyceps", "diatomic-waker", "futures-core", "pin-project-lite", + "spin 0.9.8", ] [[package]] @@ -2289,14 +2042,14 @@ dependencies = [ [[package]] name = "futures-concurrency" -version = "7.6.2" +version = "7.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b724496da7c26fcce66458526ce68fc2ecf4aaaa994281cf322ded5755520c" +checksum = "0eb68017df91f2e477ed4bea586c59eaecaa47ed885a770d0444e21e62572cd2" dependencies = [ "fixedbitset", "futures-buffered", "futures-core", - "futures-lite 1.13.0", + "futures-lite", "pin-project", "slab", "smallvec", @@ -2327,26 +2080,11 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ - "fastrand 2.1.1", + "fastrand", "futures-core", "futures-io", "parking", @@ -2361,7 +2099,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -2376,12 +2114,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.31" @@ -2439,25 +2171,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.2.12" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] @@ -2483,35 +2218,26 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] -name = "governor" -version = "0.7.0" +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0746aa765db78b521451ef74221663b57ba595bf83f75d0ce23cc09447c8139f" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "no-std-compat", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.8.5", - "smallvec", - "spinning_top", + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -2527,9 +2253,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -2556,9 +2282,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -2579,6 +2305,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2593,11 +2325,10 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.25.0-alpha.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d063c0692ee669aa6d261988aa19ca5510f1cc40e4f211024f50c888499a35d7" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", "data-encoding", @@ -2608,8 +2339,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.8.5", - "thiserror 2.0.9", + "rand 0.9.0", + "ring", + "thiserror 2.0.12", "tinyvec", "tokio", "tracing", @@ -2618,9 +2350,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.0-alpha.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc352e4412fb657e795f79b4efcf2bd60b59ee5ca0187f3554194cd1107a27" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -2629,10 +2361,10 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.8.5", + "rand 0.9.0", "resolv-conf", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -2667,28 +2399,17 @@ dependencies = [ [[package]] name = "hmac-sha256" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" +checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" [[package]] name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "hostname" -version = "0.3.1" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "libc", - "match_cfg", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -2732,9 +2453,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -2742,12 +2463,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http 1.1.0", "http-body", "pin-project-lite", @@ -2755,9 +2476,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2788,17 +2509,11 @@ dependencies = [ "libm", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -2817,9 +2532,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.1.0", @@ -2835,9 +2550,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2845,6 +2560,7 @@ dependencies = [ "http 1.1.0", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2854,9 +2570,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2990,7 +2706,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -3031,9 +2747,9 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93" +checksum = "d06464e726471718db9ad3fefc020529fabcde03313a0fc3967510e2db5add12" dependencies = [ "async-trait", "attohttpc", @@ -3044,7 +2760,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.8.5", + "rand 0.9.0", "tokio", "url", "xmltree", @@ -3052,9 +2768,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -3069,12 +2785,12 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" dependencies = [ "byteorder-lite", - "quick-error 2.0.1", + "quick-error", ] [[package]] @@ -3083,7 +2799,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -3107,11 +2823,14 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -3134,15 +2853,16 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iroh" -version = "0.30.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a59352a43dc4199fc804e1a7f3729bd14baff496fd3efbba98763e204bc4af0" +checksum = "6ca758f4ce39ae3f07de922be6c73de6a48a07f39554e78b5745585652ce38f5" dependencies = [ "aead", "anyhow", - "backoff", - "base64 0.22.1", + "atomic-waker", + "backon", "bytes", + "cfg_aliases", "concurrent-queue", "crypto_box", "data-encoding", @@ -3150,83 +2870,62 @@ dependencies = [ "derive_more", "ed25519-dalek", "futures-buffered", - "futures-concurrency", - "futures-lite 2.5.0", - "futures-sink", "futures-util", - "governor", + "getrandom 0.3.3", "hickory-resolver", - "hostname 0.4.0", "http 1.1.0", - "http-body-util", - "hyper", - "hyper-util", "igd-next", + "instant", "iroh-base", - "iroh-metrics 0.30.0", - "iroh-net-report", + "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", "iroh-quinn-udp", "iroh-relay", - "libc", + "n0-future", "netdev", - "netlink-packet-core", - "netlink-packet-route 0.19.0", - "netlink-packet-route 0.21.0", - "netlink-sys", - "netwatch 0.2.0", - "num_enum", - "once_cell", + "netwatch", "pin-project", "pkarr", "portmapper", - "postcard", "rand 0.8.5", "rcgen", "reqwest", "ring", - "rtnetlink 0.13.1", - "rtnetlink 0.14.1", "rustls", "rustls-webpki", "serde", "smallvec", - "socket2", - "strum", + "spki", + "strum 0.26.2", "stun-rs", "surge-ping", - "thiserror 2.0.9", - "time 0.3.36", + "thiserror 2.0.12", + "time", "tokio", - "tokio-rustls", "tokio-stream", - "tokio-tungstenite", - "tokio-tungstenite-wasm", "tokio-util", "tracing", "url", + "wasm-bindgen-futures", "webpki-roots", - "windows 0.58.0", - "wmi", "x509-parser", "z32", ] [[package]] name = "iroh-base" -version = "0.30.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd4101e3f0732d901beb5461cb9bc415feeda8d21281ab5bf5c0c3458eebde2" +checksum = "f91ac4aaab68153d726c4e6b39c30f9f9253743f0e25664e52f4caeb46f48d11" dependencies = [ "curve25519-dalek", "data-encoding", "derive_more", "ed25519-dalek", - "getrandom 0.2.12", "rand_core 0.6.4", "serde", - "thiserror 2.0.9", + "thiserror 2.0.12", "url", ] @@ -3245,9 +2944,9 @@ dependencies = [ [[package]] name = "iroh-gossip" -version = "0.30.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290342adf09382c1a446da805076036677d90556021f5dc427dd245404880d58" +checksum = "3ca43045ceb44b913369f417d56323fb1628ebf482ab4c1e9360e81f1b58cbc2" dependencies = [ "anyhow", "async-channel 2.3.1", @@ -3255,133 +2954,97 @@ dependencies = [ "derive_more", "ed25519-dalek", "futures-concurrency", - "futures-lite 2.5.0", + "futures-lite", "futures-util", "hex", "indexmap", "iroh", "iroh-blake3", - "iroh-metrics 0.30.0", + "iroh-metrics", + "n0-future", "postcard", "rand 0.8.5", - "rand_core 0.6.4", - "serde", - "serde-error", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "iroh-metrics" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7efd9d7437db258f4d44852beea820cd872e4db976928ee0c2bc615b8c4fe5a" -dependencies = [ - "erased_set", - "http-body-util", - "hyper", - "hyper-util", - "once_cell", - "prometheus-client", - "reqwest", + "rand_core 0.6.4", "serde", - "struct_iterable", - "thiserror 2.0.9", + "serde-error", + "thiserror 2.0.12", "tokio", + "tokio-util", "tracing", ] [[package]] name = "iroh-metrics" -version = "0.31.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571d177e20f0848a643a2c0f662be0e08968f8743b0776941f83a2152b87a180" +checksum = "f70466f14caff7420a14373676947e25e2917af6a5b1bec45825beb2bf1eb6a7" dependencies = [ - "erased_set", - "http-body-util", - "hyper", - "hyper-util", - "prometheus-client", - "reqwest", + "iroh-metrics-derive", + "itoa", "serde", - "struct_iterable", - "thiserror 2.0.9", - "tokio", + "snafu", "tracing", ] [[package]] -name = "iroh-net-report" -version = "0.30.0" +name = "iroh-metrics-derive" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee04b3b957169e3833f08791802e6bd9878213655d1adbcd9191ea78b8d671a" +checksum = "8d12f5c45c4ed2436302a4e03cad9a0ad34b2962ad0c5791e1019c0ee30eeb09" dependencies = [ - "anyhow", - "bytes", - "derive_more", - "futures-buffered", - "futures-lite 2.5.0", - "hickory-resolver", - "iroh-base", - "iroh-metrics 0.30.0", - "iroh-quinn", - "iroh-relay", - "netwatch 0.2.0", - "portmapper", - "rand 0.8.5", - "reqwest", - "rustls", - "surge-ping", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "url", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] name = "iroh-quinn" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ba75a5c57cff299d2d7ca1ddee053f66339d1756bd79ec637bcad5aa61100e" +checksum = "76c6245c9ed906506ab9185e8d7f64857129aee4f935e899f398a3bd3b70338d" dependencies = [ "bytes", + "cfg_aliases", "iroh-quinn-proto", "iroh-quinn-udp", "pin-project-lite", - "rustc-hash 2.0.0", + "rustc-hash", "rustls", "socket2", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "iroh-quinn-proto" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c869ba52683d3d067c83ab4c00a2fda18eaf13b1434d4c1352f428674d4a5d" +checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" dependencies = [ "bytes", + "getrandom 0.2.16", "rand 0.8.5", "ring", - "rustc-hash 2.0.0", + "rustc-hash", "rustls", - "rustls-platform-verifier", + "rustls-pki-types", "slab", - "thiserror 1.0.69", + "thiserror 2.0.12", "tinyvec", "tracing", + "web-time", ] [[package]] name = "iroh-quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfc0abc2fdf8cf18a6c72893b7cbebeac2274a3b1306c1760c48c0e10ac5e0" +checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", @@ -3391,56 +3054,49 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "0.30.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0080c8d0720009dc5fa109ef2ead96c5aeb8bb8e4534de8f13865520818207" +checksum = "c63f122cdfaa4b4e0e7d6d3921d2b878f42a0c6d3ee5a29456dc3f5ab5ec931f" dependencies = [ "anyhow", - "base64 0.22.1", "bytes", + "cfg_aliases", "data-encoding", "derive_more", - "futures-buffered", - "futures-lite 2.5.0", - "futures-sink", - "futures-util", - "governor", - "hickory-proto", + "getrandom 0.3.3", "hickory-resolver", - "hostname 0.4.0", "http 1.1.0", "http-body-util", "hyper", "hyper-util", "iroh-base", - "iroh-metrics 0.30.0", + "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", - "libc", - "lru", + "lru 0.12.3", + "n0-future", "num_enum", - "once_cell", "pin-project", + "pkarr", "postcard", "rand 0.8.5", "reqwest", - "ring", "rustls", "rustls-webpki", "serde", - "smallvec", - "socket2", + "sha1", + "strum 0.26.2", "stun-rs", - "thiserror 2.0.9", - "time 0.3.36", + "thiserror 2.0.12", "tokio", "tokio-rustls", - "tokio-tungstenite", - "tokio-tungstenite-wasm", "tokio-util", + "tokio-websockets", "tracing", "url", "webpki-roots", + "ws_stream_wasm", + "z32", ] [[package]] @@ -3456,9 +3112,9 @@ dependencies = [ [[package]] name = "iter-read" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b" +checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" [[package]] name = "itertools" @@ -3475,40 +3131,21 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -3542,54 +3179,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", -] - -[[package]] -name = "lettre" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#2ffdb5347f1255b4aed51cf87cda1f8207160f36" -dependencies = [ - "fast_chemail", - "log", -] - -[[package]] -name = "lettre_email" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#2ffdb5347f1255b4aed51cf87cda1f8207160f36" -dependencies = [ - "base64 0.11.0", - "email", - "lazy_static", - "lettre", - "mime", - "regex", - "time 0.1.45", - "uuid", + "spin 0.9.8", ] [[package]] name = "libc" -version = "0.2.169" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -3610,6 +3222,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + [[package]] name = "litemap" version = "0.7.4" @@ -3624,9 +3242,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3634,9 +3252,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" @@ -3673,29 +3291,35 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" + [[package]] name = "lru_time_cache" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" +[[package]] +name = "mail-builder" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5666db60ccc742381c715c8daf303e60d266f1d1c8b123ab3fe78b590edaf564" + [[package]] name = "mailparse" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da03d5980411a724e8aaf7b61a7b5e386ec55a7fb49ee3d0ff79efc7e5e7c7e" +checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f" dependencies = [ "charset", "data-encoding", "quoted_printable", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matchers" version = "0.1.0" @@ -3705,12 +3329,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "md-5" version = "0.10.6" @@ -3729,9 +3347,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -3747,21 +3365,20 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ - "adler", + "adler2", "simd-adler32", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", @@ -3793,21 +3410,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] -name = "nanorand" -version = "0.7.0" +name = "n0-future" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" dependencies = [ - "getrandom 0.2.12", + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", ] [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -3819,6 +3447,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nested_enum_utils" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa9161ed44d30e9702fe42bd78693bceac0fed02f647da749f36109023d3a3" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "netdev" version = "0.31.0" @@ -3863,26 +3503,12 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4" -dependencies = [ - "anyhow", - "byteorder", - "libc", - "log", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2" dependencies = [ "anyhow", - "bitflags 2.6.0", + "bitflags 2.8.0", "byteorder", "libc", "log", @@ -3904,17 +3530,16 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", - "thiserror 1.0.69", - "tokio", + "thiserror 2.0.12", ] [[package]] @@ -3932,67 +3557,34 @@ dependencies = [ [[package]] name = "netwatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304c0c1b348830b016039f2cb1c5ac8217084a78875262c5594925dd08aa77fc" -dependencies = [ - "anyhow", - "atomic-waker", - "bytes", - "derive_more", - "futures-lite 2.5.0", - "futures-sink", - "futures-util", - "iroh-quinn-udp", - "libc", - "netdev", - "netlink-packet-core", - "netlink-packet-route 0.19.0", - "netlink-sys", - "once_cell", - "rtnetlink 0.13.1", - "rtnetlink 0.14.1", - "serde", - "socket2", - "thiserror 2.0.9", - "time 0.3.36", - "tokio", - "tokio-util", - "tracing", - "windows 0.58.0", - "wmi", -] - -[[package]] -name = "netwatch" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64da82edf903649e6cb6a77b5a6f7fe01387d8865065d411d139018510880302" +checksum = "67eeaa5f7505c93c5a9b35ba84fd21fb8aa3f24678c76acfe8716af7862fb07a" dependencies = [ - "anyhow", "atomic-waker", "bytes", + "cfg_aliases", "derive_more", - "futures-lite 2.5.0", - "futures-sink", - "futures-util", "iroh-quinn-udp", + "js-sys", "libc", + "n0-future", + "nested_enum_utils", "netdev", "netlink-packet-core", - "netlink-packet-route 0.19.0", + "netlink-packet-route 0.23.0", + "netlink-proto", "netlink-sys", - "once_cell", - "rtnetlink 0.13.1", - "rtnetlink 0.14.1", "serde", + "snafu", "socket2", - "thiserror 2.0.9", - "time 0.3.36", + "time", "tokio", "tokio-util", "tracing", - "windows 0.58.0", + "web-sys", + "windows 0.59.0", + "windows-result 0.3.0", "wmi", ] @@ -4005,46 +3597,18 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "no-std-net" version = "0.6.0" @@ -4062,10 +3626,13 @@ dependencies = [ ] [[package]] -name = "nonzero_ext" -version = "0.3.0" +name = "nom" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] [[package]] name = "ntapi" @@ -4076,6 +3643,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.16", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4128,7 +3710,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -4142,9 +3724,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -4189,14 +3771,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "object" -version = "0.36.1" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -4224,15 +3806,19 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "opaque-debug" @@ -4242,11 +3828,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -4263,29 +3849,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.0+3.3.0" +version = "300.4.2+3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -4302,13 +3888,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "os_info" -version = "3.7.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5" dependencies = [ "log", "serde", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4331,9 +3917,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ "ecdsa", "elliptic-curve", @@ -4357,9 +3943,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -4397,9 +3983,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" @@ -4407,7 +3993,7 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -4428,12 +4014,12 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 1.0.69", + "thiserror 2.0.12", "ucd-trie", ] @@ -4457,14 +4043,14 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -4473,15 +4059,15 @@ dependencies = [ [[package]] name = "pgp" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9" +checksum = "30249ac8a98b356b473b04bc5358c75a260aa96a295d0743ce752fe7b173f235" dependencies = [ "aes", "aes-gcm", "aes-kw", "argon2", - "base64 0.21.7", + "base64", "bitfield", "block-padding", "blowfish", @@ -4514,7 +4100,7 @@ dependencies = [ "k256", "log", "md-5", - "nom", + "nom 7.1.3", "num-bigint-dig", "num-traits", "num_enum", @@ -4531,38 +4117,47 @@ dependencies = [ "sha3", "signature", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", "twofish", "x25519-dalek", - "x448", "zeroize", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -4572,26 +4167,33 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.0.3" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f9e12544b00f5561253bbd3cb72a85ff3bc398483dc1bf82bdf095c774136b" +checksum = "e32222ae3d617bf92414db29085f8a959a4515effce916e038e9399a335a0d6d" dependencies = [ + "async-compat", + "base32", "bytes", + "cfg_aliases", "document-features", + "dyn-clone", "ed25519-dalek", - "flume", - "futures", - "js-sys", - "lru", + "futures-buffered", + "futures-lite", + "getrandom 0.2.16", + "log", + "lru 0.13.0", + "ntimestamp", + "reqwest", "self_cell", + "serde", + "sha1_smol", "simple-dns", - "thiserror 1.0.69", + "thiserror 2.0.12", + "tokio", "tracing", - "ureq", - "wasm-bindgen", + "url", "wasm-bindgen-futures", - "web-sys", - "z32", ] [[package]] @@ -4617,15 +4219,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -4636,15 +4238,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -4667,7 +4269,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -4693,9 +4295,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.13" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -4729,57 +4331,59 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "portmapper" -version = "0.3.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5469b29e6ce2a27bfc9382720b5f0768993afec9e53b133d8248c8b09406156a" +checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36" dependencies = [ - "anyhow", - "base64 0.22.1", + "base64", "bytes", "derive_more", - "futures-lite 2.5.0", + "futures-lite", "futures-util", + "hyper-util", "igd-next", - "iroh-metrics 0.31.0", + "iroh-metrics", "libc", - "netwatch 0.3.0", + "nested_enum_utils", + "netwatch", "num_enum", "rand 0.8.5", "serde", "smallvec", + "snafu", "socket2", - "thiserror 2.0.9", - "time 0.3.36", + "time", "tokio", "tokio-util", + "tower-layer", "tracing", "url", ] [[package]] name = "postcard" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", - "const_format", - "embedded-io", + "embedded-io 0.4.0", + "embedded-io 0.6.1", "postcard-derive", "serde", ] [[package]] name = "postcard-derive" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4b01218787dd4420daf63875163a787a78294ad48a24e9f6fa8c6507759a79" +checksum = "0239fa9c1d225d4b7eb69925c25c5e082307a141e470573fbbe3a817ce6a7a37" dependencies = [ "proc-macro2", "quote", @@ -4794,15 +4398,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.32", +] [[package]] name = "precis-core" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73e9dd26361c32e7cd13d1032bb01c4e26a23287274e8a4e2f228cf2c9ff77b" +checksum = "9c2e7b31f132e0c6f8682cfb7bf4a5340dbe925b7986618d0826a56dfe0c8e56" dependencies = [ "precis-tools", "ucd-parse", @@ -4811,9 +4418,9 @@ dependencies = [ [[package]] name = "precis-profiles" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde4bd6624c60cb0abe2bea1dbdbb9085f629a853861e64df4abb099f8076ad4" +checksum = "dc4f67f78f50388f03494794766ba824a704db16fb5d400fe8d545fa7bc0d3f1" dependencies = [ "lazy_static", "precis-core", @@ -4823,15 +4430,21 @@ dependencies = [ [[package]] name = "precis-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07ecadec70b0f560f09abf815ae0ee1a940d38d2354c938ba7229ac7c9f5f52" +checksum = "6cc1eb2d5887ac7bfd2c0b745764db89edb84b856e4214e204ef48ef96d10c4a" dependencies = [ "lazy_static", "regex", "ucd-parse", ] +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -4879,48 +4492,25 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] -[[package]] -name = "prometheus-client" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ca959da22a332509f2a73ae9e5f23f9dcfc31fd3a54d71f159495bd5909baa" -dependencies = [ - "dtoa", - "itoa", - "parking_lot", - "prometheus-client-derive-encode", -] - -[[package]] -name = "prometheus-client-derive-encode" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "proptest" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -4952,27 +4542,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" -[[package]] -name = "quanta" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -4981,65 +4550,70 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 1.1.0", + "rustc-hash", "rustls", - "thiserror 1.0.69", + "socket2", + "thiserror 2.0.12", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", + "getrandom 0.2.16", "rand 0.8.5", "ring", - "rustc-hash 2.0.0", + "rustc-hash", "rustls", + "rustls-pki-types", "slab", - "thiserror 1.0.69", + "thiserror 2.0.12", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.2" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -5060,6 +4634,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radix_trie" version = "0.2.1" @@ -5070,19 +4650,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -5095,13 +4662,14 @@ dependencies = [ ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "rand" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", ] [[package]] @@ -5115,12 +4683,13 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "getrandom 0.1.16", + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -5129,16 +4698,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", + "getrandom 0.2.16", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "rand_core 0.5.1", + "getrandom 0.3.3", ] [[package]] @@ -5154,20 +4723,11 @@ dependencies = [ name = "ratelimit" version = "1.0.0" -[[package]] -name = "raw-cpuid" -version = "11.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" -dependencies = [ - "bitflags 2.6.0", -] - [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -5192,7 +4752,7 @@ dependencies = [ "pem", "ring", "rustls-pki-types", - "time 0.3.36", + "time", "yasna", ] @@ -5216,13 +4776,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.12", + "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -5259,9 +4819,9 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" @@ -5277,11 +4837,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "futures-util", @@ -5305,13 +4865,16 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.0", + "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", "windows-registry", @@ -5319,12 +4882,11 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" dependencies = [ - "hostname 0.3.1", - "quick-error 1.2.3", + "hostname", ] [[package]] @@ -5339,15 +4901,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.12", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -5400,49 +4961,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rtnetlink" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" -dependencies = [ - "futures", - "log", - "netlink-packet-core", - "netlink-packet-route 0.17.1", - "netlink-packet-utils", - "netlink-proto", - "netlink-sys", - "nix 0.26.4", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "rtnetlink" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5" -dependencies = [ - "futures", - "log", - "netlink-packet-core", - "netlink-packet-route 0.19.0", - "netlink-packet-utils", - "netlink-proto", - "netlink-sys", - "nix 0.27.1", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "rusqlite" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5464,21 +4989,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -5489,27 +5008,40 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "log", "once_cell", @@ -5520,62 +5052,24 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - -[[package]] -name = "rustls-platform-verifier" -version = "0.3.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-roots", - "winapi", + "web-time", ] -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.102.8" @@ -5599,7 +5093,7 @@ version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "clipboard-win", "fd-lock", @@ -5607,19 +5101,19 @@ dependencies = [ "libc", "log", "memchr", - "nix 0.29.0", + "nix", "radix_trie", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width", "utf8parse", "windows-sys 0.59.0", ] [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "salsa20" @@ -5651,18 +5145,18 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "schemars_derive", @@ -5672,14 +5166,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -5714,19 +5208,18 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", - "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -5734,19 +5227,25 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "sendfd" version = "0.4.3" @@ -5759,9 +5258,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -5777,13 +5276,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -5794,14 +5293,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -5809,16 +5308,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.7" @@ -5883,11 +5372,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -5906,12 +5401,12 @@ dependencies = [ [[package]] name = "shadowsocks" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1678a9acd37add020f89bfe05d45b9b8a6e8ad5d09f54ac2af3e0dcf0557b481" +checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f" dependencies = [ "aes", - "base64 0.22.1", + "base64", "blake3", "byte_string", "bytes", @@ -5924,15 +5419,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project", - "rand 0.8.5", + "rand 0.9.0", "sendfd", "serde", "serde_json", "serde_urlencoded", "shadowsocks-crypto", "socket2", - "spin", - "thiserror 2.0.9", + "spin 0.10.0", + "thiserror 2.0.12", "tokio", "tokio-tfo", "trait-variant", @@ -5942,9 +5437,9 @@ dependencies = [ [[package]] name = "shadowsocks-crypto" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc77ecb3a97509d22751b76665894fcffad2d10df8758f4e3f20c92ccde6bf4f" +checksum = "bda401a0ad32c82981d8862f2795713618de9bbf9768f03c17d9d145c6d805df" dependencies = [ "aes", "aes-gcm", @@ -5954,7 +5449,7 @@ dependencies = [ "chacha20poly1305", "hkdf", "md-5", - "rand 0.8.5", + "rand 0.9.0", "ring-compat", "sha1", ] @@ -5976,9 +5471,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -5999,13 +5494,19 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple-dns" -version = "0.6.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -6019,21 +5520,42 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -6044,15 +5566,12 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] -name = "spinning_top" -version = "0.3.0" +name = "spin" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" dependencies = [ "lock_api", ] @@ -6092,63 +5611,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "struct_iterable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849a064c6470a650b72e41fa6c057879b68f804d113af92900f27574828e7712" -dependencies = [ - "struct_iterable_derive", - "struct_iterable_internal", -] - -[[package]] -name = "struct_iterable_derive" -version = "0.1.0" +name = "strum" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb939ce88a43ea4e9d012f2f6b4cc789deb2db9d47bad697952a85d6978662c" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ - "erased-serde", - "proc-macro2", - "quote", - "struct_iterable_internal", - "syn 2.0.94", + "strum_macros 0.26.2", ] [[package]] -name = "struct_iterable_internal" -version = "0.1.1" +name = "strum" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9426b2a0c03e6cc2ea8dbc0168dbbf943f88755e409fb91bcb8f6a268305f4a" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" [[package]] -name = "strum" +name = "strum_macros" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "strum_macros", + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.101", ] [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "stun-rs" -version = "0.1.8" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adebf9fb8fba5c39ee34092b0383f247e4d1255b98fcffec94b4b797b85b677" +checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac" dependencies = [ - "base64 0.22.1", + "base64", "bounded-integer", "byteorder", "crc", @@ -6163,14 +5672,14 @@ dependencies = [ "precis-core", "precis-profiles", "quoted-string-parser", - "rand 0.8.5", + "rand 0.9.0", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surge-ping" @@ -6201,21 +5710,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.94" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 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 = "sync_wrapper" version = "1.0.0" @@ -6233,7 +5736,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -6252,11 +5755,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "system-configuration-sys", ] @@ -6285,14 +5788,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", - "fastrand 2.1.1", + "fastrand", + "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6313,13 +5816,13 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.1.11", + "unicode-width", ] [[package]] @@ -6333,11 +5836,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.12", ] [[package]] @@ -6348,18 +5851,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -6374,23 +5877,13 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", + "js-sys", "num-conv", "powerfmt", "serde", @@ -6406,9 +5899,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -6451,9 +5944,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -6479,20 +5972,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -6507,6 +6000,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6542,49 +6036,41 @@ dependencies = [ ] [[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-tungstenite-wasm" -version = "0.3.1" +name = "tokio-util" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e57a65894797a018b28345fa298a00c450a574aa9671e50b18218a6292a55ac" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ - "futures-channel", + "bytes", + "futures-core", + "futures-io", + "futures-sink", "futures-util", - "http 1.1.0", - "httparse", - "js-sys", - "thiserror 1.0.69", + "hashbrown", + "pin-project-lite", "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", ] [[package]] -name = "tokio-util" -version = "0.7.13" +name = "tokio-websockets" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" dependencies = [ + "base64", "bytes", "futures-core", - "futures-io", "futures-sink", - "futures-util", - "hashbrown", - "pin-project-lite", + "getrandom 0.3.3", + "http 1.1.0", + "httparse", + "rand 0.9.0", + "ring", + "rustls-pki-types", + "simdutf8", "tokio", + "tokio-rustls", + "tokio-util", ] [[package]] @@ -6623,37 +6109,36 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "pin-project", "pin-project-lite", + "sync_wrapper", "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -6663,20 +6148,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -6695,9 +6180,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -6719,7 +6204,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -6728,25 +6213,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.1.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - [[package]] name = "twofish" version = "0.7.1" @@ -6758,9 +6224,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typescript-type-def" @@ -6783,7 +6249,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -6821,9 +6287,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -6834,12 +6300,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "unicode-width" version = "0.2.0" @@ -6868,21 +6328,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" -dependencies = [ - "base64 0.22.1", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - [[package]] name = "url" version = "2.5.4" @@ -6895,12 +6340,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf16_iter" version = "1.0.5" @@ -6915,25 +6354,25 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.2.12", + "getrandom 0.3.3", "serde", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -6943,15 +6382,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.2.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -6974,21 +6407,18 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasite" @@ -6998,46 +6428,48 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7045,28 +6477,54 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -7074,9 +6532,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] @@ -7217,7 +6675,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7228,7 +6686,7 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7239,7 +6697,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7250,18 +6708,24 @@ checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-result 0.3.0", + "windows-strings 0.3.0", + "windows-targets 0.53.0", ] [[package]] @@ -7515,9 +6979,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" dependencies = [ "memchr", ] @@ -7532,17 +6996,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "wmi" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a73f536e843f309e9f7f1036561f844c0d07cf735603cb0fc230ae76e6da9aff" +checksum = "7787dacdd8e71cbc104658aade4009300777f9b5fda6a75f19145fedb8a18e71" dependencies = [ "chrono", "futures", "log", "serde", - "thiserror 2.0.9", + "thiserror 2.0.12", "windows 0.59.0", "windows-core 0.59.0", ] @@ -7559,6 +7032,25 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -7571,17 +7063,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - [[package]] name = "x509-parser" version = "0.16.0" @@ -7592,29 +7073,29 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom", + "nom 7.1.3", "oid-registry", "rusticata-macros", "thiserror 1.0.69", - "time 0.3.36", + "time", ] [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "linux-raw-sys 0.4.14", + "rustix 0.38.44", ] [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" [[package]] name = "xmltree" @@ -7637,43 +7118,40 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.36", + "time", ] [[package]] name = "yerpc" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4755f069230677a7a8d94164a521d30007c15e3be0eb4c15aa39065b9df25c8" +checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445" dependencies = [ "anyhow", "async-channel 1.9.0", - "async-mutex", + "async-lock", "async-trait", - "axum", "futures", "futures-util", "log", "schemars", "serde", "serde_json", - "tokio", - "tracing", "typescript-type-def", "yerpc_derive", ] [[package]] name = "yerpc_derive" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ba6b4417cfeb26cd806f3aabc22e7c4097632e07b5b61a4c818bccb2df4f21" +checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5" dependencies = [ "convert_case", "darling", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7696,15 +7174,15 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "synstructure", ] [[package]] name = "z32" -version = "1.1.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" @@ -7712,7 +7190,17 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ - "zerocopy-derive", + "byteorder", + "zerocopy-derive 0.7.32", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", ] [[package]] @@ -7723,7 +7211,18 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -7743,15 +7242,15 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -7764,7 +7263,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7786,7 +7285,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.101", ] [[package]] @@ -7797,9 +7296,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 85672e3faf..3584b97ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "deltachat" -version = "1.155.2" +version = "1.159.5" edition = "2021" license = "MPL-2.0" -rust-version = "1.81" -repository = "https://github.com/deltachat/deltachat-core-rust" +rust-version = "1.85" +repository = "https://github.com/chatmail/core" [profile.dev] debug = 0 @@ -41,43 +41,40 @@ ratelimit = { path = "./deltachat-ratelimit" } anyhow = { workspace = true } async-broadcast = "0.7.2" async-channel = { workspace = true } -async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] } +async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] } async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] } -async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] } +async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] } async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] } base64 = { workspace = true } -brotli = { version = "7", default-features=false, features = ["std"] } +brotli = { version = "8", default-features=false, features = ["std"] } bytes = "1" chrono = { workspace = true, features = ["alloc", "clock", "std"] } -data-encoding = "2.6.0" -email = { git = "https://github.com/deltachat/rust-email", branch = "master" } -encoded-words = "0.2" +data-encoding = "2.9.0" escaper = "0.1" fast-socks5 = "0.10" fd-lock = "4" futures-lite = { workspace = true } futures = { workspace = true } hex = "0.4.0" -hickory-resolver = "=0.25.0-alpha.4" -http-body-util = "0.1.2" +hickory-resolver = "0.25.2" +http-body-util = "0.1.3" humansize = "2" hyper = "1" -hyper-util = "0.1.10" -image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } -iroh-gossip = { version = "0.30", default-features = false, features = ["net"] } -iroh = { version = "0.30", default-features = false } +hyper-util = "0.1.11" +image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } +iroh-gossip = { version = "0.35", default-features = false, features = ["net"] } +iroh = { version = "0.35", default-features = false } kamadak-exif = "0.6.1" -lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = { workspace = true } -mailparse = "0.15" +mail-builder = { version = "0.4.2", default-features = false } +mailparse = { workspace = true } mime = "0.3.17" num_cpus = "1.16" num-derive = "0.4" num-traits = { workspace = true } -once_cell = { workspace = true } parking_lot = "0.12" percent-encoding = "2.3" -pgp = { version = "0.14.2", default-features = false } +pgp = { version = "0.15.0", default-features = false } pin-project = "1" qrcodegen = "1.7.0" quick-xml = "0.37" @@ -86,23 +83,23 @@ rand = { workspace = true } regex = { workspace = true } rusqlite = { workspace = true, features = ["sqlcipher"] } rust-hsluv = "0.1" -rustls-pki-types = "1.10.1" -rustls = { version = "0.23.20", default-features = false } +rustls-pki-types = "1.11.0" +rustls = { version = "0.23.22", default-features = false } sanitize-filename = { workspace = true } serde_json = { workspace = true } serde_urlencoded = "0.7.1" serde = { workspace = true, features = ["derive"] } sha-1 = "0.10" sha2 = "0.10" -shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] } -smallvec = "1.13.2" -strum = "0.26" -strum_macros = "0.26" +shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] } +smallvec = "1.15.0" +strum = "0.27" +strum_macros = "0.27" tagger = "4.3.4" -textwrap = "0.16.1" +textwrap = "0.16.2" thiserror = { workspace = true } tokio-io-timeout = "1.2.0" -tokio-rustls = { version = "0.26.1", default-features = false } +tokio-rustls = { version = "0.26.2", default-features = false } tokio-stream = { version = "0.1.17", features = ["fs"] } tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar tokio-util = { workspace = true } @@ -110,8 +107,8 @@ tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] } toml = "0.8" url = "2" uuid = { version = "1", features = ["serde", "v4"] } -webpki-roots = "0.26.7" -blake3 = "1.5.5" +webpki-roots = "0.26.8" +blake3 = "1.8.2" [dev-dependencies] anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests. @@ -136,6 +133,7 @@ members = [ "deltachat-time", "format-flowed", "deltachat-contact-tools", + "fuzz", ] [[bench]] @@ -159,6 +157,10 @@ harness = false name = "get_chat_msgs" harness = false +[[bench]] +name = "marknoticed_chat" +harness = false + [[bench]] name = "get_chatlist" harness = false @@ -171,29 +173,29 @@ harness = false anyhow = "1" async-channel = "2.3.1" base64 = "0.22" -chrono = { version = "0.4.39", default-features = false } +chrono = { version = "0.4.41", default-features = false } deltachat-contact-tools = { path = "deltachat-contact-tools" } deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false } deltachat = { path = ".", default-features = false } futures = "0.3.31" -futures-lite = "2.5.0" +futures-lite = "2.6.0" libc = "0.2" log = "0.4" +mailparse = "0.16.1" nu-ansi-term = "0.46" num-traits = "0.2" -once_cell = "1.20.2" rand = "0.8" regex = "1.10" rusqlite = "0.32" sanitize-filename = "0.5" serde = "1.0" serde_json = "1" -tempfile = "3.14.0" -thiserror = "1" +tempfile = "3.19.1" +thiserror = "2" tokio = "1" -tokio-util = "0.7.13" +tokio-util = "0.7.14" tracing-subscriber = "0.3" -yerpc = "0.6.2" +yerpc = "0.6.4" [features] default = ["vendored"] diff --git a/README.md b/README.md index 1e8d6f437e..2d6a536980 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,41 @@

- Delta Chat Logo +Chatmail logo

- - Rust CI + + Rust CI - - dependency status + + dependency status

-

-The core library for Delta Chat, written in Rust -

+The chatmail core library implements low-level network and encryption protocols, +integrated by many chat bots and higher level applications, +allowing to securely participate in the globally scaled e-mail server network. +We provide reproducibly-built `deltachat-rpc-server` static binaries +that offer a stdio-based high-level JSON-RPC API for instant messaging purposes. + +The following protocols are handled without requiring API users to know much about them: + +- secure TLS setup with DNS caching and shadowsocks/proxy support + +- robust [SMTP](https://github.com/chatmail/async-imap) + and [IMAP](https://github.com/chatmail/async-smtp) handling + +- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse) + and [MIME building](https://github.com/stalwartlabs/mail-builder). + +- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp) + and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io) + +- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and + [webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime). + +- a simulation- and real-world tested [P2P group membership + protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership). + ## Installing Rust and Cargo @@ -27,12 +49,12 @@ $ curl https://sh.rustup.rs -sSf | sh ## Using the CLI client -Compile and run Delta Chat Core command line utility, using `cargo`: +Compile and run the command line utility, using `cargo`: ``` -$ cargo run --locked -p deltachat-repl -- ~/deltachat-db +$ cargo run --locked -p deltachat-repl -- ~/profile-db ``` -where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist. +where ~/profile-db is the database file. The utility will create it if it does not exist. Optionally, install `deltachat-repl` binary with ``` @@ -40,13 +62,13 @@ $ cargo install --locked --path deltachat-repl/ ``` and run as ``` -$ deltachat-repl ~/deltachat-db +$ deltachat-repl ~/profile-db ``` Configure your account (if not already configured): ``` -Delta Chat Core is awaiting your commands. +Chatmail is awaiting your commands. > set addr your@email.org > set mail_pw yourpassword > configure @@ -84,11 +106,6 @@ Single#10: yourfriends@email.org [yourfriends@email.org] Message sent. ``` -If `yourfriend@email.org` uses DeltaChat, but does not receive message just -sent, it is advisable to check `Spam` folder. It is known that at least -`gmx.com` treat such test messages as spam, unless told otherwise with web -interface. - List messages when inside a chat: ``` @@ -104,7 +121,7 @@ For more commands type: ## Installing libdeltachat system wide ``` -$ git clone https://github.com/deltachat/deltachat-core-rust.git +$ git clone https://github.com/chatmail/core.git $ cd deltachat-core-rust $ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr $ cmake --build build @@ -139,7 +156,7 @@ $ cargo test -- --ignored Install [`cargo-bolero`](https://github.com/camshaft/bolero) with ```sh -$ cargo install cargo-bolero +$ cargo install cargo-bolero@0.8.0 ``` Run fuzzing tests with @@ -165,20 +182,16 @@ $ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE ## Update Provider Data To add the updates from the -[provider-db](https://github.com/deltachat/provider-db) to the core, run: - -``` -./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs -``` +[provider-db](https://github.com/chatmail/provider-db) to the core, +check line `REV=` inside `./scripts/update-provider-database.sh` +and then run the script. ## Language bindings and frontend projects Language bindings are available for: - **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\] -- **Node.js** - - over JSON-RPC: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\] - - over CFFI[^1]: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat/)\] +- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\] - **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\] - **Go** - over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\] diff --git a/RELEASE.md b/RELEASE.md index f150716cb8..177cceb389 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,12 +2,12 @@ For example, to release version 1.116.0 of the core, do the following steps. -1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker). +1. Resolve all [blocker issues](https://github.com/chatmail/core/labels/blocker). 2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`. 3. add a link to compare previous with current version to the end of CHANGELOG.md: - `[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0` + `[1.116.0]: https://github.com/chatmail/core/compare/v1.115.2...v1.116.0` 4. Update the version by running `scripts/set_core_version.py 1.116.0`. diff --git a/benches/marknoticed_chat.rs b/benches/marknoticed_chat.rs new file mode 100644 index 0000000000..8cdc0f96d8 --- /dev/null +++ b/benches/marknoticed_chat.rs @@ -0,0 +1,94 @@ +#![recursion_limit = "256"] +use std::path::Path; + +use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion}; +use deltachat::chat::{self, ChatId}; +use deltachat::chatlist::Chatlist; +use deltachat::context::Context; +use deltachat::stock_str::StockStrings; +use deltachat::Events; +use futures_lite::future::block_on; +use tempfile::tempdir; + +async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) { + for c in chats.iter().take(20) { + chat::marknoticed_chat(context, *c).await.unwrap(); + } +} + +fn criterion_benchmark(c: &mut Criterion) { + // To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many + // messages, such as your primary account. + if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let chats: Vec<_> = rt.block_on(async { + let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new()) + .await + .unwrap(); + let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap(); + let len = chatlist.len(); + (1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect() + }); + + // This mainly tests the performance of marknoticed_chat() + // when nothing has to be done + c.bench_function( + "chat::marknoticed_chat (mark 20 chats as noticed repeatedly)", + |b| { + let dir = tempdir().unwrap(); + let dir = dir.path(); + let new_db = dir.join("dc.db"); + std::fs::copy(&path, &new_db).unwrap(); + + let context = block_on(async { + Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new()) + .await + .unwrap() + }); + + b.to_async(&rt) + .iter(|| marknoticed_chat_benchmark(&context, black_box(&chats))) + }, + ); + + // If the first 20 chats contain fresh messages or reactions, + // this tests the performance of marking them as noticed. + c.bench_function( + "chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)", + |b| { + b.to_async(&rt).iter_batched( + || { + let dir = tempdir().unwrap(); + let new_db = dir.path().join("dc.db"); + std::fs::copy(&path, &new_db).unwrap(); + + let context = block_on(async { + Context::new( + Path::new(&new_db), + 100, + Events::new(), + StockStrings::new(), + ) + .await + .unwrap() + }); + (dir, context) + }, + |(_dir, context)| { + let chats = &chats; + async move { + marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await + } + }, + BatchSize::PerIteration, + ); + }, + ); + } else { + println!("env var not set: DELTACHAT_BENCHMARK_DATABASE"); + } +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/cliff.toml b/cliff.toml index 658fe4dcea..b264f420ef 100644 --- a/cliff.toml +++ b/cliff.toml @@ -11,7 +11,7 @@ filter_unconventional = false split_commits = false # regex for preprocessing the commit messages commit_preprocessors = [ - { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/chatmail/core/pull/${2}))"}, # replace pull request / issue numbers ] # regex for parsing and grouping commits commit_parsers = [ @@ -82,11 +82,11 @@ footer = """ {% if release.version -%} {% if release.previous.version -%} [{{ release.version | trim_start_matches(pat="v") }}]: \ - https://github.com/deltachat/deltachat-core-rust\ + https://github.com/chatmail/core\ /compare/{{ release.previous.version }}..{{ release.version }} {% endif -%} {% else -%} - [unreleased]: https://github.com/deltachat/deltachat-core-rust\ + [unreleased]: https://github.com/chatmail/core\ /compare/{{ release.previous.version }}..HEAD {% endif -%} {% endfor %} diff --git a/deltachat-contact-tools/Cargo.toml b/deltachat-contact-tools/Cargo.toml index 2057732579..ec1f4c7561 100644 --- a/deltachat-contact-tools/Cargo.toml +++ b/deltachat-contact-tools/Cargo.toml @@ -9,7 +9,6 @@ license = "MPL-2.0" [dependencies] anyhow = { workspace = true } -once_cell = { workspace = true } regex = { workspace = true } rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature. chrono = { workspace = true, features = ["alloc", "clock", "std"] } diff --git a/deltachat-contact-tools/src/lib.rs b/deltachat-contact-tools/src/lib.rs index 17c2ad0d4c..86e947d678 100644 --- a/deltachat-contact-tools/src/lib.rs +++ b/deltachat-contact-tools/src/lib.rs @@ -29,203 +29,14 @@ use std::fmt; use std::ops::Deref; +use std::sync::LazyLock; use anyhow::bail; -use anyhow::Context as _; use anyhow::Result; -use chrono::{DateTime, NaiveDateTime}; -use once_cell::sync::Lazy; use regex::Regex; -#[derive(Debug)] -/// A Contact, as represented in a VCard. -pub struct VcardContact { - /// The email address, vcard property `email` - pub addr: String, - /// This must be the name authorized by the contact itself, not a locally given name. Vcard - /// property `fn`. Can be empty, one should use `display_name()` to obtain the display name. - pub authname: String, - /// The contact's public PGP key in Base64, vcard property `key` - pub key: Option, - /// The contact's profile image (=avatar) in Base64, vcard property `photo` - pub profile_image: Option, - /// The timestamp when the vcard was created / last updated, vcard property `rev` - pub timestamp: Result, -} - -impl VcardContact { - /// Returns the contact's display name. - pub fn display_name(&self) -> &str { - match self.authname.is_empty() { - false => &self.authname, - true => &self.addr, - } - } -} - -/// Returns a vCard containing given contacts. -/// -/// Calling [`parse_vcard()`] on the returned result is a reverse operation. -pub fn make_vcard(contacts: &[VcardContact]) -> String { - fn format_timestamp(c: &VcardContact) -> Option { - let timestamp = *c.timestamp.as_ref().ok()?; - let datetime = DateTime::from_timestamp(timestamp, 0)?; - Some(datetime.format("%Y%m%dT%H%M%SZ").to_string()) - } - - let mut res = "".to_string(); - for c in contacts { - let addr = &c.addr; - let display_name = c.display_name(); - res += &format!( - "BEGIN:VCARD\n\ - VERSION:4.0\n\ - EMAIL:{addr}\n\ - FN:{display_name}\n" - ); - if let Some(key) = &c.key { - res += &format!("KEY:data:application/pgp-keys;base64,{key}\n"); - } - if let Some(profile_image) = &c.profile_image { - res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n"); - } - if let Some(timestamp) = format_timestamp(c) { - res += &format!("REV:{timestamp}\n"); - } - res += "END:VCARD\n"; - } - res -} - -/// Parses `VcardContact`s from a given `&str`. -pub fn parse_vcard(vcard: &str) -> Vec { - fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { - let start_of_s = s.get(..prefix.len())?; - - if start_of_s.eq_ignore_ascii_case(prefix) { - s.get(prefix.len()..) - } else { - None - } - } - fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> { - let remainder = remove_prefix(s, property)?; - // If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`, - // then `remainder` is now `;TYPE=work:alice@example.com` - - // Note: This doesn't handle the case where there are quotes around a colon, - // like `NAME;Foo="Some quoted text: that contains a colon":value`. - // This could be improved in the future, but for now, the parsing is good enough. - let (params, value) = remainder.split_once(':')?; - // In the example from above, `params` is now `;TYPE=work` - // and `value` is now `alice@example.com` - - if params - .chars() - .next() - .filter(|c| !c.is_ascii_punctuation() || *c == '_') - .is_some() - { - // `s` started with `property`, but the next character after it was not punctuation, - // so this line's property is actually something else - return None; - } - Some(value) - } - fn parse_datetime(datetime: &str) -> Result { - // According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp - // is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses - // ISO.8601, but fails to parse any of the examples given. - // So, instead just parse using a format string. - - // Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500. - let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") { - Ok(datetime) => datetime.timestamp(), - // Parses 19961022T140000. - Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") { - Ok(datetime) => datetime - .and_local_timezone(chrono::offset::Local) - .single() - .context("Could not apply local timezone to parsed date and time")? - .timestamp(), - Err(_) => return Err(e.into()), - }, - }; - Ok(timestamp) - } - - // Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2 - static NEWLINE_AND_SPACE_OR_TAB: Lazy = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap()); - let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, ""); - - let mut lines = unfolded_lines.lines().peekable(); - let mut contacts = Vec::new(); - - while lines.peek().is_some() { - // Skip to the start of the vcard: - for line in lines.by_ref() { - if line.eq_ignore_ascii_case("BEGIN:VCARD") { - break; - } - } - - let mut display_name = None; - let mut addr = None; - let mut key = None; - let mut photo = None; - let mut datetime = None; - - for mut line in lines.by_ref() { - if let Some(remainder) = remove_prefix(line, "item1.") { - // Remove the group name, if the group is called "item1". - // If necessary, we can improve this to also remove groups that are called something different that "item1". - // - // Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos. - line = remainder; - } - - if let Some(email) = vcard_property(line, "email") { - addr.get_or_insert(email); - } else if let Some(name) = vcard_property(line, "fn") { - display_name.get_or_insert(name); - } else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:") - .or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:")) - .or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,")) - .or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,")) - { - key.get_or_insert(k); - } else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:") - .or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:")) - .or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:")) - .or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:")) - .or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:")) - .or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:")) - .or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,")) - { - photo.get_or_insert(p); - } else if let Some(rev) = vcard_property(line, "rev") { - datetime.get_or_insert(rev); - } else if line.eq_ignore_ascii_case("END:VCARD") { - break; - } - } - - let (authname, addr) = - sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or("")); - - contacts.push(VcardContact { - authname, - addr, - key: key.map(|s| s.to_string()), - profile_image: photo.map(|s| s.to_string()), - timestamp: datetime - .context("No timestamp in vcard") - .and_then(parse_datetime), - }); - } - - contacts -} +mod vcard; +pub use vcard::{make_vcard, parse_vcard, VcardContact}; /// Valid contact address. #[derive(Debug, Clone)] @@ -277,7 +88,8 @@ impl rusqlite::types::ToSql for ContactAddress { /// - Removes special characters from the name, see [`sanitize_name()`] /// - Removes the name if it is equal to the address by setting it to "" pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) { - static ADDR_WITH_NAME_REGEX: Lazy = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap()); + static ADDR_WITH_NAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap()); let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) { ( if name.is_empty() { @@ -479,124 +291,8 @@ impl rusqlite::types::ToSql for EmailAddress { #[cfg(test)] mod tests { - use chrono::TimeZone; - use super::*; - #[test] - fn test_vcard_thunderbird() { - let contacts = parse_vcard( - "BEGIN:VCARD -VERSION:4.0 -FN:'Alice Mueller' -EMAIL;PREF=1:alice.mueller@posteo.de -UID:a8083264-ca47-4be7-98a8-8ec3db1447ca -END:VCARD -BEGIN:VCARD -VERSION:4.0 -FN:'bobzzz@freenet.de' -EMAIL;PREF=1:bobzzz@freenet.de -UID:cac4fef4-6351-4854-bbe4-9b6df857eaed -END:VCARD -", - ); - - assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string()); - assert_eq!(contacts[0].authname, "Alice Mueller".to_string()); - assert_eq!(contacts[0].key, None); - assert_eq!(contacts[0].profile_image, None); - assert!(contacts[0].timestamp.is_err()); - - assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string()); - assert_eq!(contacts[1].authname, "".to_string()); - assert_eq!(contacts[1].key, None); - assert_eq!(contacts[1].profile_image, None); - assert!(contacts[1].timestamp.is_err()); - - assert_eq!(contacts.len(), 2); - } - - #[test] - fn test_vcard_simple_example() { - let contacts = parse_vcard( - "BEGIN:VCARD -VERSION:4.0 -FN:Alice Wonderland -N:Wonderland;Alice;;;Ms. -GENDER:W -EMAIL;TYPE=work:alice@example.com -KEY;TYPE=PGP;ENCODING=b:[base64-data] -REV:20240418T184242Z - -END:VCARD", - ); - - assert_eq!(contacts[0].addr, "alice@example.com".to_string()); - assert_eq!(contacts[0].authname, "Alice Wonderland".to_string()); - assert_eq!(contacts[0].key, Some("[base64-data]".to_string())); - assert_eq!(contacts[0].profile_image, None); - assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762); - - assert_eq!(contacts.len(), 1); - } - - #[test] - fn test_make_and_parse_vcard() { - let contacts = [ - VcardContact { - addr: "alice@example.org".to_string(), - authname: "Alice Wonderland".to_string(), - key: Some("[base64-data]".to_string()), - profile_image: Some("image in Base64".to_string()), - timestamp: Ok(1713465762), - }, - VcardContact { - addr: "bob@example.com".to_string(), - authname: "".to_string(), - key: None, - profile_image: None, - timestamp: Ok(0), - }, - ]; - let items = [ - "BEGIN:VCARD\n\ - VERSION:4.0\n\ - EMAIL:alice@example.org\n\ - FN:Alice Wonderland\n\ - KEY:data:application/pgp-keys;base64,[base64-data]\n\ - PHOTO:data:image/jpeg;base64,image in Base64\n\ - REV:20240418T184242Z\n\ - END:VCARD\n", - "BEGIN:VCARD\n\ - VERSION:4.0\n\ - EMAIL:bob@example.com\n\ - FN:bob@example.com\n\ - REV:19700101T000000Z\n\ - END:VCARD\n", - ]; - let mut expected = "".to_string(); - for len in 0..=contacts.len() { - let contacts = &contacts[0..len]; - let vcard = make_vcard(contacts); - if len > 0 { - expected += items[len - 1]; - } - assert_eq!(vcard, expected); - let parsed = parse_vcard(&vcard); - assert_eq!(parsed.len(), contacts.len()); - for i in 0..parsed.len() { - assert_eq!(parsed[i].addr, contacts[i].addr); - assert_eq!(parsed[i].authname, contacts[i].authname); - assert_eq!(parsed[i].key, contacts[i].key); - assert_eq!(parsed[i].profile_image, contacts[i].profile_image); - assert_eq!( - parsed[i].timestamp.as_ref().unwrap(), - contacts[i].timestamp.as_ref().unwrap() - ); - } - } - } - #[test] fn test_contact_address() -> Result<()> { let alice_addr = "alice@example.org"; @@ -643,112 +339,6 @@ END:VCARD", assert_eq!(EmailAddress::new("@d.tt").is_ok(), false); } - #[test] - fn test_vcard_android() { - let contacts = parse_vcard( - "BEGIN:VCARD -VERSION:2.1 -N:;Bob;;; -FN:Bob -TEL;CELL:+1-234-567-890 -EMAIL;HOME:bob@example.org -END:VCARD -BEGIN:VCARD -VERSION:2.1 -N:;Alice;;; -FN:Alice -EMAIL;HOME:alice@example.org -END:VCARD -", - ); - - assert_eq!(contacts[0].addr, "bob@example.org".to_string()); - assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(contacts[0].key, None); - assert_eq!(contacts[0].profile_image, None); - - assert_eq!(contacts[1].addr, "alice@example.org".to_string()); - assert_eq!(contacts[1].authname, "Alice".to_string()); - assert_eq!(contacts[1].key, None); - assert_eq!(contacts[1].profile_image, None); - - assert_eq!(contacts.len(), 2); - } - - #[test] - fn test_vcard_local_datetime() { - let contacts = parse_vcard( - "BEGIN:VCARD\n\ - VERSION:4.0\n\ - FN:Alice Wonderland\n\ - EMAIL;TYPE=work:alice@example.org\n\ - REV:20240418T184242\n\ - END:VCARD", - ); - assert_eq!(contacts.len(), 1); - assert_eq!(contacts[0].addr, "alice@example.org".to_string()); - assert_eq!(contacts[0].authname, "Alice Wonderland".to_string()); - assert_eq!( - *contacts[0].timestamp.as_ref().unwrap(), - chrono::offset::Local - .with_ymd_and_hms(2024, 4, 18, 18, 42, 42) - .unwrap() - .timestamp() - ); - } - - #[test] - fn test_vcard_with_base64_avatar() { - // This is not an actual base64-encoded avatar, it's just to test the parsing. - // This one is Android-like. - let vcard0 = "BEGIN:VCARD -VERSION:2.1 -N:;Bob;;; -FN:Bob -EMAIL;HOME:bob@example.org -PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU - AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA - L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q== - -END:VCARD -"; - // This one is DOS-like. - let vcard1 = vcard0.replace('\n', "\r\n"); - for vcard in [vcard0, vcard1.as_str()] { - let contacts = parse_vcard(vcard); - assert_eq!(contacts.len(), 1); - assert_eq!(contacts[0].addr, "bob@example.org".to_string()); - assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(contacts[0].key, None); - assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="); - } - } - - #[test] - fn test_protonmail_vcard() { - let contacts = parse_vcard( - "BEGIN:VCARD -VERSION:4.0 -FN;PREF=1:Alice Wonderland -UID:proton-web-03747582-328d-38dc-5ddd-000000000000 -ITEM1.EMAIL;PREF=1:alice@example.org -ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -ITEM1.X-PM-ENCRYPT:true -ITEM1.X-PM-SIGN:true -END:VCARD", - ); - - assert_eq!(contacts.len(), 1); - assert_eq!(&contacts[0].addr, "alice@example.org"); - assert_eq!(&contacts[0].authname, "Alice Wonderland"); - assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - assert!(contacts[0].timestamp.is_err()); - assert_eq!(contacts[0].profile_image, None); - } - #[test] fn test_sanitize_name() { assert_eq!(&sanitize_name(" hello world "), "hello world"); diff --git a/deltachat-contact-tools/src/vcard.rs b/deltachat-contact-tools/src/vcard.rs new file mode 100644 index 0000000000..0ccd811047 --- /dev/null +++ b/deltachat-contact-tools/src/vcard.rs @@ -0,0 +1,232 @@ +use std::sync::LazyLock; + +use anyhow::Context as _; +use anyhow::Result; +use chrono::DateTime; +use chrono::NaiveDateTime; +use regex::Regex; + +use crate::sanitize_name_and_addr; + +#[derive(Debug)] +/// A Contact, as represented in a VCard. +pub struct VcardContact { + /// The email address, vcard property `email` + pub addr: String, + /// This must be the name authorized by the contact itself, not a locally given name. Vcard + /// property `fn`. Can be empty, one should use `display_name()` to obtain the display name. + pub authname: String, + /// The contact's public PGP key in Base64, vcard property `key` + pub key: Option, + /// The contact's profile image (=avatar) in Base64, vcard property `photo` + pub profile_image: Option, + /// The timestamp when the vcard was created / last updated, vcard property `rev` + pub timestamp: Result, +} + +impl VcardContact { + /// Returns the contact's display name. + pub fn display_name(&self) -> &str { + match self.authname.is_empty() { + false => &self.authname, + true => &self.addr, + } + } +} + +/// Returns a vCard containing given contacts. +/// +/// Calling [`parse_vcard()`] on the returned result is a reverse operation. +pub fn make_vcard(contacts: &[VcardContact]) -> String { + fn format_timestamp(c: &VcardContact) -> Option { + let timestamp = *c.timestamp.as_ref().ok()?; + let datetime = DateTime::from_timestamp(timestamp, 0)?; + Some(datetime.format("%Y%m%dT%H%M%SZ").to_string()) + } + + let mut res = "".to_string(); + for c in contacts { + let addr = &c.addr; + let display_name = c.display_name(); + res += &format!( + "BEGIN:VCARD\r\n\ + VERSION:4.0\r\n\ + EMAIL:{addr}\r\n\ + FN:{display_name}\r\n" + ); + if let Some(key) = &c.key { + res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n"); + } + if let Some(profile_image) = &c.profile_image { + res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n"); + } + if let Some(timestamp) = format_timestamp(c) { + res += &format!("REV:{timestamp}\r\n"); + } + res += "END:VCARD\r\n"; + } + res +} + +/// Parses `VcardContact`s from a given `&str`. +pub fn parse_vcard(vcard: &str) -> Vec { + fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { + let start_of_s = s.get(..prefix.len())?; + + if start_of_s.eq_ignore_ascii_case(prefix) { + s.get(prefix.len()..) + } else { + None + } + } + /// Returns (parameters, value) tuple. + fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> { + let remainder = remove_prefix(line, property)?; + // If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`, + // then `remainder` is now `;TYPE=work:alice@example.com` + + // Note: This doesn't handle the case where there are quotes around a colon, + // like `NAME;Foo="Some quoted text: that contains a colon":value`. + // This could be improved in the future, but for now, the parsing is good enough. + let (mut params, value) = remainder.split_once(':')?; + // In the example from above, `params` is now `;TYPE=work` + // and `value` is now `alice@example.com` + + if params + .chars() + .next() + .filter(|c| !c.is_ascii_punctuation() || *c == '_') + .is_some() + { + // `s` started with `property`, but the next character after it was not punctuation, + // so this line's property is actually something else + return None; + } + if let Some(p) = remove_prefix(params, ";") { + params = p; + } + if let Some(p) = remove_prefix(params, "PREF=1") { + params = p; + } + Some((params, value)) + } + fn base64_key(line: &str) -> Option<&str> { + let (params, value) = vcard_property(line, "key")?; + if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64") + || params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b") + { + return Some(value); + } + if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,") + .or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,")) + { + return Some(value); + } + + None + } + fn base64_photo(line: &str) -> Option<&str> { + let (params, value) = vcard_property(line, "photo")?; + if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64") + || params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG") + || params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b") + || params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG") + || params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG") + || params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64") + { + return Some(value); + } + if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,") + .or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,")) + { + return Some(value); + } + + None + } + fn parse_datetime(datetime: &str) -> Result { + // According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp + // is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses + // ISO.8601, but fails to parse any of the examples given. + // So, instead just parse using a format string. + + // Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500. + let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") { + Ok(datetime) => datetime.timestamp(), + // Parses 19961022T140000. + Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") { + Ok(datetime) => datetime + .and_local_timezone(chrono::offset::Local) + .single() + .context("Could not apply local timezone to parsed date and time")? + .timestamp(), + Err(_) => return Err(e.into()), + }, + }; + Ok(timestamp) + } + + // Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2 + static NEWLINE_AND_SPACE_OR_TAB: LazyLock = + LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap()); + let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, ""); + + let mut lines = unfolded_lines.lines().peekable(); + let mut contacts = Vec::new(); + + while lines.peek().is_some() { + // Skip to the start of the vcard: + for line in lines.by_ref() { + if line.eq_ignore_ascii_case("BEGIN:VCARD") { + break; + } + } + + let mut display_name = None; + let mut addr = None; + let mut key = None; + let mut photo = None; + let mut datetime = None; + + for mut line in lines.by_ref() { + if let Some(remainder) = remove_prefix(line, "item1.") { + // Remove the group name, if the group is called "item1". + // If necessary, we can improve this to also remove groups that are called something different that "item1". + // + // Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos. + line = remainder; + } + + if let Some((_params, email)) = vcard_property(line, "email") { + addr.get_or_insert(email); + } else if let Some((_params, name)) = vcard_property(line, "fn") { + display_name.get_or_insert(name); + } else if let Some(k) = base64_key(line) { + key.get_or_insert(k); + } else if let Some(p) = base64_photo(line) { + photo.get_or_insert(p); + } else if let Some((_params, rev)) = vcard_property(line, "rev") { + datetime.get_or_insert(rev); + } else if line.eq_ignore_ascii_case("END:VCARD") { + let (authname, addr) = + sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or("")); + + contacts.push(VcardContact { + authname, + addr, + key: key.map(|s| s.to_string()), + profile_image: photo.map(|s| s.to_string()), + timestamp: datetime + .context("No timestamp in vcard") + .and_then(parse_datetime), + }); + break; + } + } + } + + contacts +} + +#[cfg(test)] +mod vcard_tests; diff --git a/deltachat-contact-tools/src/vcard/vcard_tests.rs b/deltachat-contact-tools/src/vcard/vcard_tests.rs new file mode 100644 index 0000000000..cd2742b484 --- /dev/null +++ b/deltachat-contact-tools/src/vcard/vcard_tests.rs @@ -0,0 +1,274 @@ +use chrono::TimeZone as _; + +use super::*; + +#[test] +fn test_vcard_thunderbird() { + let contacts = parse_vcard( + "BEGIN:VCARD +VERSION:4.0 +FN:'Alice Mueller' +EMAIL;PREF=1:alice.mueller@posteo.de +UID:a8083264-ca47-4be7-98a8-8ec3db1447ca +END:VCARD +BEGIN:VCARD +VERSION:4.0 +FN:'bobzzz@freenet.de' +EMAIL;PREF=1:bobzzz@freenet.de +UID:cac4fef4-6351-4854-bbe4-9b6df857eaed +END:VCARD +", + ); + + assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string()); + assert_eq!(contacts[0].authname, "Alice Mueller".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image, None); + assert!(contacts[0].timestamp.is_err()); + + assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string()); + assert_eq!(contacts[1].authname, "".to_string()); + assert_eq!(contacts[1].key, None); + assert_eq!(contacts[1].profile_image, None); + assert!(contacts[1].timestamp.is_err()); + + assert_eq!(contacts.len(), 2); +} + +#[test] +fn test_vcard_simple_example() { + let contacts = parse_vcard( + "BEGIN:VCARD +VERSION:4.0 +FN:Alice Wonderland +N:Wonderland;Alice;;;Ms. +GENDER:W +EMAIL;TYPE=work:alice@example.com +KEY;TYPE=PGP;ENCODING=b:[base64-data] +REV:20240418T184242Z + +END:VCARD", + ); + + assert_eq!(contacts[0].addr, "alice@example.com".to_string()); + assert_eq!(contacts[0].authname, "Alice Wonderland".to_string()); + assert_eq!(contacts[0].key, Some("[base64-data]".to_string())); + assert_eq!(contacts[0].profile_image, None); + assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762); + + assert_eq!(contacts.len(), 1); +} + +#[test] +fn test_vcard_with_trailing_newline() { + let contacts = parse_vcard( + "BEGIN:VCARD\r +VERSION:4.0\r +FN:Alice Wonderland\r +N:Wonderland;Alice;;;Ms.\r +GENDER:W\r +EMAIL;TYPE=work:alice@example.com\r +KEY;TYPE=PGP;ENCODING=b:[base64-data]\r +REV:20240418T184242Z\r +END:VCARD\r +\r", + ); + + assert_eq!(contacts[0].addr, "alice@example.com".to_string()); + assert_eq!(contacts[0].authname, "Alice Wonderland".to_string()); + assert_eq!(contacts[0].key, Some("[base64-data]".to_string())); + assert_eq!(contacts[0].profile_image, None); + assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762); + + assert_eq!(contacts.len(), 1); +} + +#[test] +fn test_make_and_parse_vcard() { + let contacts = [ + VcardContact { + addr: "alice@example.org".to_string(), + authname: "Alice Wonderland".to_string(), + key: Some("[base64-data]".to_string()), + profile_image: Some("image in Base64".to_string()), + timestamp: Ok(1713465762), + }, + VcardContact { + addr: "bob@example.com".to_string(), + authname: "".to_string(), + key: None, + profile_image: None, + timestamp: Ok(0), + }, + ]; + let items = [ + "BEGIN:VCARD\r\n\ + VERSION:4.0\r\n\ + EMAIL:alice@example.org\r\n\ + FN:Alice Wonderland\r\n\ + KEY:data:application/pgp-keys;base64,[base64-data]\r\n\ + PHOTO:data:image/jpeg;base64,image in Base64\r\n\ + REV:20240418T184242Z\r\n\ + END:VCARD\r\n", + "BEGIN:VCARD\r\n\ + VERSION:4.0\r\n\ + EMAIL:bob@example.com\r\n\ + FN:bob@example.com\r\n\ + REV:19700101T000000Z\r\n\ + END:VCARD\r\n", + ]; + let mut expected = "".to_string(); + for len in 0..=contacts.len() { + let contacts = &contacts[0..len]; + let vcard = make_vcard(contacts); + if len > 0 { + expected += items[len - 1]; + } + assert_eq!(vcard, expected); + let parsed = parse_vcard(&vcard); + assert_eq!(parsed.len(), contacts.len()); + for i in 0..parsed.len() { + assert_eq!(parsed[i].addr, contacts[i].addr); + assert_eq!(parsed[i].authname, contacts[i].authname); + assert_eq!(parsed[i].key, contacts[i].key); + assert_eq!(parsed[i].profile_image, contacts[i].profile_image); + assert_eq!( + parsed[i].timestamp.as_ref().unwrap(), + contacts[i].timestamp.as_ref().unwrap() + ); + } + } +} + +#[test] +fn test_vcard_android() { + let contacts = parse_vcard( + "BEGIN:VCARD +VERSION:2.1 +N:;Bob;;; +FN:Bob +TEL;CELL:+1-234-567-890 +EMAIL;HOME:bob@example.org +END:VCARD +BEGIN:VCARD +VERSION:2.1 +N:;Alice;;; +FN:Alice +EMAIL;HOME:alice@example.org +END:VCARD +", + ); + + assert_eq!(contacts[0].addr, "bob@example.org".to_string()); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image, None); + + assert_eq!(contacts[1].addr, "alice@example.org".to_string()); + assert_eq!(contacts[1].authname, "Alice".to_string()); + assert_eq!(contacts[1].key, None); + assert_eq!(contacts[1].profile_image, None); + + assert_eq!(contacts.len(), 2); +} + +#[test] +fn test_vcard_local_datetime() { + let contacts = parse_vcard( + "BEGIN:VCARD\n\ + VERSION:4.0\n\ + FN:Alice Wonderland\n\ + EMAIL;TYPE=work:alice@example.org\n\ + REV:20240418T184242\n\ + END:VCARD", + ); + assert_eq!(contacts.len(), 1); + assert_eq!(contacts[0].addr, "alice@example.org".to_string()); + assert_eq!(contacts[0].authname, "Alice Wonderland".to_string()); + assert_eq!( + *contacts[0].timestamp.as_ref().unwrap(), + chrono::offset::Local + .with_ymd_and_hms(2024, 4, 18, 18, 42, 42) + .unwrap() + .timestamp() + ); +} + +#[test] +fn test_vcard_with_base64_avatar() { + // This is not an actual base64-encoded avatar, it's just to test the parsing. + // This one is Android-like. + let vcard0 = "BEGIN:VCARD +VERSION:2.1 +N:;Bob;;; +FN:Bob +EMAIL;HOME:bob@example.org +PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU + AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA + L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q== + +END:VCARD +"; + // This one is DOS-like. + let vcard1 = vcard0.replace('\n', "\r\n"); + for vcard in [vcard0, vcard1.as_str()] { + let contacts = parse_vcard(vcard); + assert_eq!(contacts.len(), 1); + assert_eq!(contacts[0].addr, "bob@example.org".to_string()); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="); + } +} + +#[test] +fn test_protonmail_vcard() { + let contacts = parse_vcard( + "BEGIN:VCARD +VERSION:4.0 +FN;PREF=1:Alice Wonderland +UID:proton-web-03747582-328d-38dc-5ddd-000000000000 +ITEM1.EMAIL;PREF=1:alice@example.org +ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +ITEM1.X-PM-ENCRYPT:true +ITEM1.X-PM-SIGN:true +END:VCARD", + ); + + assert_eq!(contacts.len(), 1); + assert_eq!(&contacts[0].addr, "alice@example.org"); + assert_eq!(&contacts[0].authname, "Alice Wonderland"); + assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert!(contacts[0].timestamp.is_err()); + assert_eq!(contacts[0].profile_image, None); +} + +/// Proton at some point slightly changed the format of their vcards +#[test] +fn test_protonmail_vcard2() { + let contacts = parse_vcard( + r"BEGIN:VCARD +VERSION:4.0 +FN;PREF=1:Alice +PHOTO;PREF=1:data:image/jpeg;base64,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z +REV:Invalid Date +ITEM1.EMAIL;PREF=1:alice@example.org +KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa== +UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +END:VCARD", + ); + + assert_eq!(contacts.len(), 1); + assert_eq!(&contacts[0].addr, "alice@example.org"); + assert_eq!(&contacts[0].authname, "Alice"); + assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="); + assert!(contacts[0].timestamp.is_err()); + assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z"); +} diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 632a84f953..d768690f81 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.155.2" +version = "1.159.5" description = "Deltachat FFI" edition = "2018" readme = "README.md" @@ -24,7 +24,6 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } anyhow = { workspace = true } thiserror = { workspace = true } rand = { workspace = true } -once_cell = { workspace = true } yerpc = { workspace = true, features = ["anyhow_expose"] } [features] diff --git a/deltachat-ffi/Doxyfile b/deltachat-ffi/Doxyfile index 5c60f15e37..8ef948710e 100644 --- a/deltachat-ffi/Doxyfile +++ b/deltachat-ffi/Doxyfile @@ -1,7 +1,7 @@ -# Doxyfile 1.8.11 +# Doxyfile 1.13.2 # This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. +# Doxygen (www.doxygen.org) for a project. # # All text after a double hash (##) is considered a comment and is placed in # front of the TAG it is preceding. @@ -12,16 +12,26 @@ # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use Doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use Doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- -# This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all text -# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv -# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv -# for the list of possible encodings. +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. # The default value is: UTF-8. DOXYFILE_ENCODING = UTF-8 @@ -33,17 +43,19 @@ DOXYFILE_ENCODING = UTF-8 # The default value is: My Project. ###################################################### + PROJECT_NAME = "Delta Chat Core C Interface" -###################################################### # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version # control system is used. +###################################################### + PROJECT_NUMBER = # Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a +# for a project that appears at the top of each page and should give viewers a # quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = @@ -55,26 +67,46 @@ PROJECT_BRIEF = PROJECT_LOGO = Doxyfile-logo.png +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If +# entered, it will be relative to the location where Doxygen was started. If # left blank the current directory will be used. ###################################################### + OUTPUT_DIRECTORY = . -###################################################### -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. +# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding Doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise cause +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. # The default value is: NO. +###################################################### + CREATE_SUBDIRS = NO -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode # U+3044. @@ -83,28 +115,28 @@ CREATE_SUBDIRS = NO ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this +# documentation generated by Doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. # The default value is: YES. BRIEF_MEMBER_DESC = YES -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief # description of a member or function before the detailed description # # Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the @@ -125,13 +157,13 @@ REPEAT_BRIEF = YES ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief +# Doxygen will generate a detailed section even if there is only a brief # description. # The default value is: NO. ALWAYS_DETAILED_SEC = NO -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. @@ -139,7 +171,7 @@ ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = NO -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path # before files name in the file list and in the header files. If set to NO the # shortest path that makes the file name unique will be used # The default value is: YES. @@ -149,11 +181,11 @@ FULL_PATH_NAMES = YES # The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. # Stripping is only done if one of the specified strings matches the left-hand # part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to +# If left blank the directory from which Doxygen is run is used as the path to # strip. # # Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. +# will be relative from the directory where Doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. STRIP_FROM_PATH = @@ -167,33 +199,46 @@ STRIP_FROM_PATH = STRIP_FROM_INC_PATH = -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't +# If the SHORT_NAMES tag is set to YES, Doxygen will generate much shorter (but +# less readable) file names. This can be useful if your file system doesn't # support long names like on DOS, Mac, or CD-ROM. # The default value is: NO. SHORT_NAMES = NO -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot, question mark or exclamation mark) of a +# Javadoc-style comment as the brief description. If set to NO, the Javadoc- +# style will behave just like regular Qt-style comments (thus requiring an +# explicit @brief command for a brief description.) # The default value is: NO. ###################################################### + JAVADOC_AUTOBRIEF = YES -###################################################### -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by Doxygen. # The default value is: NO. +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot, question mark or exclamation mark) of a Qt-style +# comment as the brief description. If set to NO, the Qt-style will behave just +# like regular Qt-style comments (thus requiring an explicit \brief command for +# a brief description.) +# The default value is: NO. + +###################################################### + QT_AUTOBRIEF = NO -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a # multi-line C++ special comment block (i.e. a block of //! or /// comments) as # a brief description. This used to be the default behavior. The new default is # to treat a multi-line C++ comment block as a detailed description. Set this @@ -205,13 +250,21 @@ QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO +# By default Python docstrings are displayed as preformatted text and Doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. INHERIT_DOCS = YES -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new # page for each member. If set to NO, the documentation of a member will be part # of the file/class/namespace that contains it. # The default value is: NO. @@ -228,11 +281,16 @@ TAB_SIZE = 4 # the documentation. An alias has the form: # name=value # For example adding -# "sideeffect=@par Side Effects:\n" +# "sideeffect=@par Side Effects:^^" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines. +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) ALIASES = @@ -243,9 +301,8 @@ ALIASES = # The default value is: NO. ###################################################### -# we do not set this for Delta Chat as we use C in an object-orientated way, so "Class" is better than "Data structure" for us. !! + OPTIMIZE_OUTPUT_FOR_C = NO -###################################################### # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or # Python sources only. Doxygen will then generate output that is more tailored @@ -253,6 +310,8 @@ OPTIMIZE_OUTPUT_FOR_C = NO # qualified scopes will look different, etc. # The default value is: NO. +###################################################### + OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran @@ -267,49 +326,90 @@ OPTIMIZE_FOR_FORTRAN = NO OPTIMIZE_OUTPUT_VHDL = NO +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, -# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: -# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: -# Fortran. In the later case the parser tries to guess whether the code is fixed -# or free formatted code, this is the default for Fortran type files), VHDL. For -# instance to make doxygen treat .inc files as Fortran files (default is PHP), -# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# language is one of the parsers supported by Doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make Doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. # # Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. +# the files are not read by Doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. EXTENSION_MAPPING = -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments # according to the Markdown format, which allows for more readable -# documentation. See http://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in # case of backward compatibilities issues. # The default value is: YES. MARKDOWN_SUPPORT = YES -# When enabled doxygen tries to link words that correspond to documented +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 6. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 6 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled Doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. +# globally by setting AUTOLINK_SUPPORT to NO. Words listed in the +# AUTOLINK_IGNORE_WORDS tag are excluded from automatic linking. # The default value is: YES. AUTOLINK_SUPPORT = YES +# This tag specifies a list of words that, when matching the start of a word in +# the documentation, will suppress auto links generation, if it is enabled via +# AUTOLINK_SUPPORT. This list does not affect affect links explicitly created +# using \# or the \link or commands. +# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. + +AUTOLINK_IGNORE_WORDS = + # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and +# tag to YES in order to let Doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. # The default value is: NO. BUILTIN_STL_SUPPORT = NO @@ -321,16 +421,16 @@ BUILTIN_STL_SUPPORT = NO CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. +# https://www.riverbankcomputing.com/software) sources only. Doxygen will parse +# them like normal C++ but will assume all classes use public instead of private +# inheritance when no explicit protection keyword is present. # The default value is: NO. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate # getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. +# Doxygen to replace the get and set methods by a property in the documentation. # This will only work if the methods are indeed getting or setting a simple # type. If this is not the case, or you want to show the methods anyway, you # should set this option to NO. @@ -339,7 +439,7 @@ SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first +# tag is set to YES then Doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. # The default value is: NO. @@ -397,21 +497,42 @@ TYPEDEF_HIDES_STRUCT = NO # The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This # cache is used to resolve symbols given their name and scope. Since this can be # an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The +# code, Doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# Doxygen will become slower. If the cache is too large, memory is wasted. The # cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range # is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest +# symbols. At the end of a run Doxygen will report the cache usage and suggest # the optimal cache size from a speed point of view. # Minimum value: 0, maximum value: 9, default value: 0. LOOKUP_CACHE_SIZE = 0 +# The NUM_PROC_THREADS specifies the number of threads Doxygen is allowed to use +# during processing. When set to 0 Doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in # documentation are documented, even if no documentation was available. Private # class members and static file members will be hidden unless the # EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. @@ -427,6 +548,12 @@ EXTRACT_ALL = NO EXTRACT_PRIVATE = NO +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + # If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal # scope will be included in the documentation. # The default value is: NO. @@ -446,8 +573,8 @@ EXTRACT_STATIC = NO # The default value is: YES. ###################################################### + EXTRACT_LOCAL_CLASSES = NO -###################################################### # This flag is only useful for Objective-C code. If set to YES, local methods, # which are defined in the implementation section but not in the interface are @@ -455,6 +582,8 @@ EXTRACT_LOCAL_CLASSES = NO # included. # The default value is: NO. +###################################################### + EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be @@ -466,7 +595,14 @@ EXTRACT_LOCAL_METHODS = NO EXTRACT_ANON_NSPACES = NO -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation # section is generated. This option has no effect if EXTRACT_ALL is enabled. @@ -474,22 +610,31 @@ EXTRACT_ANON_NSPACES = NO HIDE_UNDOC_MEMBERS = NO -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = NO -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# (class|struct|union) declarations. If set to NO, these declarations will be -# included in the documentation. +# If the HIDE_UNDOC_NAMESPACES tag is set to YES, Doxygen will hide all +# undocumented namespaces that are normally visible in the namespace hierarchy. +# If set to NO, these namespaces will be included in the various overviews. This +# option has no effect if EXTRACT_ALL is enabled. +# The default value is: YES. + +HIDE_UNDOC_NAMESPACES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. # The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. If set to NO, these # blocks will be appended to the function's detailed documentation block. # The default value is: NO. @@ -503,30 +648,44 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. -# The default value is: system dependent. +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and macOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. CASE_SENSE_NAMES = YES -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with # their full class and namespace scopes in the documentation. If set to YES, the # scope will be hidden. # The default value is: NO. HIDE_SCOPE_NAMES = NO -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will # append additional text to a page's title, such as Class Reference. If set to # YES the compound reference will be hidden. # The default value is: NO. HIDE_COMPOUND_REFERENCE= NO -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -539,7 +698,7 @@ SHOW_INCLUDE_FILES = YES SHOW_GROUPED_MEMB_INC = NO -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include # files with double quotes in the documentation rather than with sharp brackets. # The default value is: NO. @@ -551,14 +710,14 @@ FORCE_LOCAL_INCLUDES = NO INLINE_INFO = YES -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the # (detailed) documentation of file and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. # The default value is: YES. SORT_MEMBER_DOCS = YES -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member # name. If set to NO, the members will appear in declaration order. Note that # this will also influence the order of the classes in the class list. @@ -566,7 +725,7 @@ SORT_MEMBER_DOCS = YES SORT_BRIEF_DOCS = YES -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the # (brief and detailed) documentation of class members so that constructors and # destructors are listed first. If set to NO the constructors will appear in the # respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. @@ -578,7 +737,7 @@ SORT_BRIEF_DOCS = YES SORT_MEMBERS_CTORS_1ST = NO -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy # of group names into alphabetical order. If set to NO the group names will # appear in their defined order. # The default value is: NO. @@ -595,11 +754,11 @@ SORT_GROUP_NAMES = YES SORT_BY_SCOPE_NAME = NO -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper # type resolution of all parameters of a function it will reject a match between # the prototype and the implementation of a member function even if there is # only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still # accept a match between prototype and implementation in such cases. # The default value is: NO. @@ -669,24 +828,25 @@ SHOW_FILES = NO SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from +# Doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command command input-file, where command is the value of the # FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file +# by Doxygen. Whatever the program writes to standard output is used as the file # version. For an example see the documentation. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated +# by Doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can +# that represents Doxygen's defaults, run Doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. # -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE # tag is left empty. LAYOUT_FILE = @@ -694,26 +854,42 @@ LAYOUT_FILE = # The CITE_BIB_FILES tag can be used to specify one or more bib files containing # the reference definitions. This must be a list of .bib files. The .bib # extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. # For LaTeX the style of the bibliography can be controlled using # LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the # search path. See also \cite for info how to create references. CITE_BIB_FILES = +# The EXTERNAL_TOOL_PATH tag can be used to extend the search path (PATH +# environment variable) so that external tools such as latex and gs can be +# found. +# Note: Directories specified with EXTERNAL_TOOL_PATH are added in front of the +# path already specified by the PATH variable, and are added in the order +# specified. +# Note: This option is particularly useful for macOS version 14 (Sonoma) and +# higher, when running Doxygen from Doxywizard, because in this case any user- +# defined changes to the PATH are ignored. A typical example on macOS is to set +# EXTERNAL_TOOL_PATH = /Library/TeX/texbin /usr/local/bin +# together with the standard path, the full search path used by doxygen when +# launching external tools will then become +# PATH=/Library/TeX/texbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +EXTERNAL_TOOL_PATH = + #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the +# standard output by Doxygen. If QUIET is set to YES this implies that the # messages are off. # The default value is: NO. QUIET = YES # The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES # this implies that the warnings are on. # # Tip: Turn warnings on while writing the documentation. @@ -721,48 +897,97 @@ QUIET = YES WARNINGS = YES -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate # warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: YES. WARN_IF_UNDOCUMENTED = YES -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = YES +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. +# value. If set to NO, Doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC # The default value is: NO. WARN_NO_PARAMDOC = NO -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If WARN_LAYOUT_FILE option is set to YES, Doxygen will warn about issues found +# while parsing the user defined layout file, such as missing or wrong elements. +# See also LAYOUT_FILE for details. If set to NO, problems with the layout file +# will be suppressed. +# The default value is: YES. + +WARN_LAYOUT_FILE = YES + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO -# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen # can produce. The string should contain the $file, $line, and $text tags, which # will be replaced by the file and line number from which the warning originated # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard -# error (stderr). +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). WARN_LOGFILE = @@ -777,31 +1002,48 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. ###################################################### + INPUT = deltachat.h -###################################################### # This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: http://www.gnu.org/software/libiconv) for the list of -# possible encodings. +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING # The default value is: UTF-8. +###################################################### + INPUT_ENCODING = UTF-8 +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). +# See also: INPUT_ENCODING for further information on supported encodings. + +INPUT_FILE_ENCODING = + # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. +# read by Doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f, *.for, *.tcl, -# *.vhd, *.vhdl, *.ucf, *.qsf, *.as and *.js. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = @@ -815,7 +1057,7 @@ RECURSIVE = NO # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. # -# Note that relative paths are relative to the directory from which doxygen is +# Note that relative paths are relative to the directory from which Doxygen is # run. EXCLUDE = @@ -840,21 +1082,35 @@ EXCLUDE_PATTERNS = # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* +# ANamespace::AClass, ANamespace::*Test ###################################################### -EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t -EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t -EXCLUDE_SYMBOLS += _dc_* jsmn* -###################################################### + +EXCLUDE_SYMBOLS = dc_aheader_t \ + dc_apeerstate_t \ + dc_e2ee_helper_t \ + dc_imap_t \ + dc_job*_t \ + dc_key_t \ + dc_loginparam_t \ + dc_mime*_t \ + dc_saxparser_t \ + dc_simplify_t \ + dc_smtp_t \ + dc_sqlite3_t \ + dc_strbuilder_t \ + dc_param_t \ + dc_hash_t \ + dc_hashelem_t \ + _dc_* \ + jsmn* # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include # command). +###################################################### + EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the @@ -877,7 +1133,7 @@ EXAMPLE_RECURSIVE = NO IMAGE_PATH = -# The INPUT_FILTER tag can be used to specify a program that doxygen should +# The INPUT_FILTER tag can be used to specify a program that Doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command: # @@ -892,9 +1148,14 @@ IMAGE_PATH = # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # +# Note that Doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. +# properly processed by Doxygen. INPUT_FILTER = @@ -907,7 +1168,7 @@ INPUT_FILTER = # # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. +# properly processed by Doxygen. FILTER_PATTERNS = @@ -929,10 +1190,28 @@ FILTER_SOURCE_PATTERNS = # If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that # is part of the input, its contents will be placed on the main page # (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. +# and want to reuse the introduction page also for the Doxygen output. USE_MDFILE_AS_MAINPAGE = +# If the IMPLICIT_DIR_DOCS tag is set to YES, any README.md file found in sub- +# directories of the project's root, is used as the documentation for that sub- +# directory, except when the README.md starts with a \dir, \page or \mainpage +# command. If set to NO, the README.md file needs to start with an explicit \dir +# command in order to be used as directory documentation. +# The default value is: YES. + +IMPLICIT_DIR_DOCS = YES + +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- @@ -945,18 +1224,19 @@ USE_MDFILE_AS_MAINPAGE = # The default value is: NO. ###################################################### -# we do use a browser as there is a high risk the user sees the wrong code! -# however, this may change in the future + SOURCE_BROWSER = NO -###################################################### # Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. +# multi-line macros, enums or list initialized variables directly into the +# documentation. # The default value is: NO. +###################################################### + INLINE_SOURCES = NO -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct Doxygen to hide any # special comment blocks from generated source code fragments. Normal C, C++ and # Fortran comments will always remain visible. # The default value is: YES. @@ -964,7 +1244,7 @@ INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# function all documented functions referencing it will be listed. +# entity all documented functions referencing it will be listed. # The default value is: NO. REFERENCED_BY_RELATION = NO @@ -994,28 +1274,28 @@ REFERENCES_LINK_SOURCE = YES SOURCE_TOOLTIPS = YES # If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# point to the HTML generated by the htags(1) tool instead of Doxygen built-in # source browser. The htags tool is part of GNU's global source tagging system -# (see http://www.gnu.org/software/global/global.html). You will need version +# (see https://www.gnu.org/software/global/global.html). You will need version # 4.8.6 or higher. # # To use it do the following: # - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file # - Make sure the INPUT points to the root of the source tree # - Run doxygen as normal # # Doxygen will invoke htags (and that will in turn invoke gtags), so these # tools must be available from the command line (i.e. in the search path). # -# The result: instead of the source browser generated by doxygen, the links to +# The result: instead of the source browser generated by Doxygen, the links to # source code will now point to the output of htags. # The default value is: NO. # This tag requires that the tag SOURCE_BROWSER is set to YES. USE_HTAGS = NO -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# If the VERBATIM_HEADERS tag is set the YES then Doxygen will generate a # verbatim copy of the header file for each class for which an include is # specified. Set to NO to disable this. # See also: Section \class. @@ -1023,6 +1303,46 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES +# If the CLANG_ASSISTED_PARSING tag is set to YES then Doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which Doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then Doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by Doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- @@ -1033,20 +1353,14 @@ VERBATIM_HEADERS = YES # The default value is: YES. ###################################################### -ALPHABETICAL_INDEX = NO -###################################################### - -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. -COLS_IN_ALPHA_INDEX = 5 +ALPHABETICAL_INDEX = NO -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = @@ -1055,7 +1369,7 @@ IGNORE_PREFIX = # Configuration options related to the HTML output #--------------------------------------------------------------------------- -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output # The default value is: YES. GENERATE_HTML = YES @@ -1076,40 +1390,40 @@ HTML_OUTPUT = html HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a +# each generated HTML page. If the tag is left blank Doxygen will generate a # standard header. # # To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. +# that Doxygen needs, which is dependent on the configuration options used (e.g. # the setting GENERATE_TREEVIEW). It is highly recommended to start with a # default header using # doxygen -w html new_header.html new_footer.html new_stylesheet.css # YourConfigFile # and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally +# for information on how to generate the default header that Doxygen normally # uses. # Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description +# default header when upgrading to a newer version of Doxygen. For a description # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard +# generated HTML page. If the tag is left blank Doxygen will generate a standard # footer. See HTML_HEADER for more information on how to generate a default # footer and what special commands can be used inside the footer. See also # section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. +# that Doxygen normally uses. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading style # sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. +# the HTML output. If left blank Doxygen will generate a default style sheet. # See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. +# sheet that Doxygen normally uses. # Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as # it is more robust and this tag (HTML_STYLESHEET) will in the future become # obsolete. @@ -1119,18 +1433,23 @@ HTML_STYLESHEET = # The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined # cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. +# created by Doxygen. Using this option one can overrule certain style aspects. # This is preferred over using HTML_STYLESHEET since it does not replace the # standard style sheet and is therefore more robust against future updates. # Doxygen will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. # This tag requires that the tag GENERATE_HTML is set to YES. ###################################################### + HTML_EXTRA_STYLESHEET = Doxyfile.css -###################################################### # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note @@ -1140,12 +1459,27 @@ HTML_EXTRA_STYLESHEET = Doxyfile.css # files will be copied as-is; there are no commands or markers available. # This tag requires that the tag GENERATE_HTML is set to YES. +###################################################### + HTML_EXTRA_FILES = +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see -# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. # Minimum value: 0, maximum value: 359, default value: 220. @@ -1154,7 +1488,7 @@ HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A +# in the HTML output. For a value of 0 the output will use gray-scales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1172,14 +1506,16 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_TIMESTAMP = NO +HTML_DYNAMIC_MENUS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the @@ -1189,6 +1525,33 @@ HTML_TIMESTAMP = NO HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1204,13 +1567,14 @@ HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: http://developer.apple.com/tools/xcode/), introduced with -# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, Doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html -# for more information. +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1224,6 +1588,13 @@ GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doxygen generated docs" +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. @@ -1246,14 +1617,18 @@ DOCSET_PUBLISHER_ID = org.doxygen.Publisher DOCSET_PUBLISHER_NAME = Publisher -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# If the GENERATE_HTMLHELP tag is set to YES then Doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline (the HTML help workshop was already many +# years in maintenance mode). You can download the HTML help workshop from the +# web archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML # files are now used as the Windows 98 help format, and will replace the old # Windows help format (.hlp) on all Windows platforms in the future. Compressed # HTML files also contain an index, a table of contents, and you can search for @@ -1273,14 +1648,14 @@ CHM_FILE = # The HHC_LOCATION tag can be used to specify the location (absolute path # including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. +# Doxygen will try to run the HTML help compiler on the generated index.hhp. # The file has to be specified with full path. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). +# (YES) or that it should be included in the main .chm file (NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. @@ -1307,6 +1682,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -1325,7 +1710,8 @@ QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace -# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1333,8 +1719,8 @@ QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- -# folders). +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1342,30 +1728,30 @@ QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's filter section matches. Qt Help Project / Filter Attributes (see: -# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_SECT_FILTER_ATTRS = -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty Doxygen will try to +# run qhelpgenerator on the generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = @@ -1396,7 +1782,7 @@ ECLIPSE_DOC_ID = org.doxygen.Project # of each HTML page. A value of NO enables the index and the value YES disables # it. Since the tabs in the index contain the same information as the navigation # tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -# The default value is: NO. +# The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. DISABLE_INDEX = NO @@ -1408,18 +1794,30 @@ DISABLE_INDEX = NO # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by Doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = NO +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. +# Doxygen will group on one line in the generated HTML documentation. # # Note that a value of 0 will completely suppress the enum values from appearing # in the overview section. @@ -1428,6 +1826,12 @@ GENERATE_TREEVIEW = NO ENUM_VALUES_PER_LINE = 4 +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used # to set the initial width (in pixels) of the frame in which the tree is shown. # Minimum value: 0, maximum value: 1500, default value: 250. @@ -1435,35 +1839,48 @@ ENUM_VALUES_PER_LINE = 4 TREEVIEW_WIDTH = 250 -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to # external symbols imported via tag files in a separate window. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = NO +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, Doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML +# Doxygen run you need to manually remove any form_*.png images from the HTML # output directory to force them to be regenerated. # Minimum value: 8, maximum value: 50, default value: 10. # This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. -FORMULA_TRANSPARENT = YES +FORMULA_MACROFILE = # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# http://www.mathjax.org) which uses client side Javascript for the rendering +# https://www.mathjax.org) which uses client side JavaScript for the rendering # instead of using pre-rendered bitmaps. Use this if you do not have LaTeX # installed or if you want to formulas look prettier in the HTML output. When # enabled you may also need to install MathJax separately and configure the path @@ -1473,11 +1890,29 @@ FORMULA_TRANSPARENT = YES USE_MATHJAX = NO +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + # When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). # Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for MathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1490,33 +1925,40 @@ MATHJAX_FORMAT = HTML-CSS # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of -# MathJax from http://www.mathjax.org before deployment. -# The default value is: http://cdn.mathjax.org/mathjax/latest. +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces # of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an # example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_CODEFILE = -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and # should work on any modern browser. Note that when using HTML help # (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) # there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then +# For large projects the JavaScript based search engine can be slow, then # enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to # search using the keyboard; to jump to the search box use + S # (what the is depends on the OS and browser, but it is typically @@ -1533,9 +1975,9 @@ MATHJAX_CODEFILE = SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be -# implemented using a web server instead of a web client using Javascript. There +# implemented using a web server instead of a web client using JavaScript. There # are two flavors of web server based searching depending on the EXTERNAL_SEARCH -# setting. When disabled, doxygen will generate a PHP script for searching and +# setting. When disabled, Doxygen will generate a PHP script for searching and # an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing # and searching needs to be provided by external tools. See the section # "External Indexing and Searching" for details. @@ -1544,7 +1986,7 @@ SEARCHENGINE = YES SERVER_BASED_SEARCH = NO -# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP +# When EXTERNAL_SEARCH tag is enabled Doxygen will no longer generate the PHP # script for searching. Instead the search results are written to an XML file # which needs to be processed by an external indexer. Doxygen will invoke an # external search engine pointed to by the SEARCHENGINE_URL option to obtain the @@ -1552,7 +1994,8 @@ SERVER_BASED_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: http://xapian.org/). +# Xapian (see: +# https://xapian.org/). # # See the section "External Indexing and Searching" for details. # The default value is: NO. @@ -1565,8 +2008,9 @@ EXTERNAL_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: http://xapian.org/). See the section "External Indexing and -# Searching" for details. +# Xapian (see: +# https://xapian.org/). See the section "External Indexing and Searching" for +# details. # This tag requires that the tag SEARCHENGINE is set to YES. SEARCHENGINE_URL = @@ -1587,7 +2031,7 @@ SEARCHDATA_FILE = searchdata.xml EXTERNAL_SEARCH_ID = -# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through Doxygen # projects other than the one defined by this configuration file, but that are # all added to the same external search index. Each project needs to have a # unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of @@ -1601,12 +2045,12 @@ EXTRA_SEARCH_MAPPINGS = # Configuration options related to the LaTeX output #--------------------------------------------------------------------------- -# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. +# If the GENERATE_LATEX tag is set to YES, Doxygen will generate LaTeX output. # The default value is: YES. ###################################################### + GENERATE_LATEX = NO -###################################################### # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -1614,27 +2058,43 @@ GENERATE_LATEX = NO # The default directory is: latex. # This tag requires that the tag GENERATE_LATEX is set to YES. +###################################################### + LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. # -# Note that when enabling USE_PDFLATEX this option is only used for generating -# bitmaps for formulas in the HTML output, but not in the Makefile that is -# written to the output directory. -# The default file is: latex. +# Note that when not enabling USE_PDFLATEX the default is latex when enabling +# USE_PDFLATEX the default is pdflatex and when in the later case latex is +# chosen this is overwritten by pdflatex. For specific output languages the +# default can have been set differently, this depends on the implementation of +# the output language. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate # index for LaTeX. +# Note: This tag is used in the Makefile / make.bat. +# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file +# (.tex). # The default file is: makeindex. # This tag requires that the tag GENERATE_LATEX is set to YES. MAKEINDEX_CMD_NAME = makeindex -# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX +# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to +# generate index for LaTeX. In case there is no backslash (\) as first character +# it will be automatically added in the LaTeX code. +# Note: This tag is used in the generated output file (.tex). +# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. +# The default value is: makeindex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_MAKEINDEX_CMD = makeindex + +# If the COMPACT_LATEX tag is set to YES, Doxygen generates more compact LaTeX # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -1663,36 +2123,38 @@ PAPER_TYPE = a4 EXTRA_PACKAGES = -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -# generated LaTeX document. The header should contain everything until the first -# chapter. If it is left blank doxygen will generate a standard header. See -# section "Doxygen usage" for information on how to let doxygen write the -# default header to a separate file. +# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for +# the generated LaTeX document. The header should contain everything until the +# first chapter. If it is left blank Doxygen will generate a standard header. It +# is highly recommended to start with a default header using +# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty +# and then modify the file new_header.tex. See also section "Doxygen usage" for +# information on how to generate the default header that Doxygen normally uses. # -# Note: Only use a user-defined header if you know what you are doing! The -# following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -# string, for the replacement values of the other commands the user is referred -# to HTML_HEADER. +# Note: Only use a user-defined header if you know what you are doing! +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of Doxygen. The following +# commands have a special meaning inside the header (and footer): For a +# description of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HEADER = -# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -# generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. See +# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for +# the generated LaTeX document. The footer should contain everything after the +# last chapter. If it is left blank Doxygen will generate a standard footer. See # LATEX_HEADER for more information on how to generate a default footer and what -# special commands can be used inside the footer. -# -# Note: Only use a user-defined footer if you know what you are doing! +# special commands can be used inside the footer. See also section "Doxygen +# usage" for information on how to generate the default footer that Doxygen +# normally uses. Note: Only use a user-defined footer if you know what you are +# doing! # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_FOOTER = # The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined # LaTeX style sheets that are included after the standard style sheets created -# by doxygen. Using this option one can overrule certain style aspects. Doxygen +# by Doxygen. Using this option one can overrule certain style aspects. Doxygen # will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the @@ -1718,61 +2180,59 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = YES -# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES, to get a -# higher quality PDF documentation. +# If the USE_PDFLATEX tag is set to YES, Doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. # The default value is: YES. # This tag requires that the tag GENERATE_LATEX is set to YES. USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. This option is also used -# when generating formulas in HTML. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_BATCHMODE = NO -# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the +# If the LATEX_HIDE_INDICES tag is set to YES then Doxygen will not include the # index chapters (such as File Index, Compound Index, etc.) in the output. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HIDE_INDICES = NO -# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -# code with syntax highlighting in the LaTeX output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_SOURCE_CODE = NO - # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See -# http://en.wikipedia.org/wiki/BibTeX and \cite for more info. -# The default value is: plain. +# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. +# The default value is: plainnat. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. +# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) +# path from which the emoji images will be read. If a relative path is entered, +# it will be relative to the LATEX_OUTPUT directory. If left blank the +# LATEX_OUTPUT directory will be used. # This tag requires that the tag GENERATE_LATEX is set to YES. -LATEX_TIMESTAMP = NO +LATEX_EMOJI_DIRECTORY = #--------------------------------------------------------------------------- # Configuration options related to the RTF output #--------------------------------------------------------------------------- -# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The +# If the GENERATE_RTF tag is set to YES, Doxygen will generate RTF output. The # RTF output is optimized for Word 97 and may not look too pretty with other RTF # readers/editors. # The default value is: NO. @@ -1787,7 +2247,7 @@ GENERATE_RTF = NO RTF_OUTPUT = rtf -# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF +# If the COMPACT_RTF tag is set to YES, Doxygen generates more compact RTF # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -1807,38 +2267,36 @@ COMPACT_RTF = NO RTF_HYPERLINKS = NO -# Load stylesheet definitions from file. Syntax is similar to doxygen's config -# file, i.e. a series of assignments. You only have to provide replacements, -# missing definitions are set to their default value. +# Load stylesheet definitions from file. Syntax is similar to Doxygen's +# configuration file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. # # See also section "Doxygen usage" for information on how to generate the -# default style sheet that doxygen normally uses. +# default style sheet that Doxygen normally uses. # This tag requires that the tag GENERATE_RTF is set to YES. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an RTF document. Syntax is -# similar to doxygen's config file. A template extensions file can be generated -# using doxygen -e rtf extensionFile. +# similar to Doxygen's configuration file. A template extensions file can be +# generated using doxygen -e rtf extensionFile. # This tag requires that the tag GENERATE_RTF is set to YES. RTF_EXTENSIONS_FILE = -# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -# with syntax highlighting in the RTF output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. +# The RTF_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the RTF_OUTPUT output directory. +# Note that the files will be copied as-is; there are no commands or markers +# available. # This tag requires that the tag GENERATE_RTF is set to YES. -RTF_SOURCE_CODE = NO +RTF_EXTRA_FILES = #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- -# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for +# If the GENERATE_MAN tag is set to YES, Doxygen will generate man pages for # classes and files. # The default value is: NO. @@ -1869,7 +2327,7 @@ MAN_EXTENSION = .3 MAN_SUBDIR = -# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, then it # will generate one additional man file for each entity documented in the real # man page(s). These additional files only source the real man page, but without # them the man command would be unable to find the correct page. @@ -1882,7 +2340,7 @@ MAN_LINKS = NO # Configuration options related to the XML output #--------------------------------------------------------------------------- -# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that +# If the GENERATE_XML tag is set to YES, Doxygen will generate an XML file that # captures the structure of the code including all documentation. # The default value is: NO. @@ -1896,7 +2354,7 @@ GENERATE_XML = YES XML_OUTPUT = xml -# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program +# If the XML_PROGRAMLISTING tag is set to YES, Doxygen will dump the program # listings (including syntax highlighting and cross-referencing information) to # the XML output. Note that enabling this will significantly increase the size # of the XML output. @@ -1905,11 +2363,18 @@ XML_OUTPUT = xml XML_PROGRAMLISTING = YES +# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, Doxygen will include +# namespace members in file scope as well, matching the HTML output. +# The default value is: NO. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_NS_MEMB_FILE_SCOPE = NO + #--------------------------------------------------------------------------- # Configuration options related to the DOCBOOK output #--------------------------------------------------------------------------- -# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files +# If the GENERATE_DOCBOOK tag is set to YES, Doxygen will generate Docbook files # that can be used to generate PDF. # The default value is: NO. @@ -1923,32 +2388,49 @@ GENERATE_DOCBOOK = NO DOCBOOK_OUTPUT = docbook -# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -# program listings (including syntax highlighting and cross-referencing -# information) to the DOCBOOK output. Note that enabling this will significantly -# increase the size of the DOCBOOK output. +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES, Doxygen will generate an +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures +# the structure of the code including all documentation. Note that this feature +# is still experimental and incomplete at the moment. # The default value is: NO. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. -DOCBOOK_PROGRAMLISTING = NO +GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- -# Configuration options for the AutoGen Definitions output +# Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- -# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sf.net) file that captures the -# structure of the code including all documentation. Note that this feature is -# still experimental and incomplete at the moment. +# If the GENERATE_SQLITE3 tag is set to YES Doxygen will generate a Sqlite3 +# database with symbols found by Doxygen stored in tables. # The default value is: NO. -GENERATE_AUTOGEN_DEF = NO +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each Doxygen run. If set to NO, Doxygen +# will warn if a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- -# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module +# If the GENERATE_PERLMOD tag is set to YES, Doxygen will generate a Perl module # file that captures the structure of the code including all documentation. # # Note that this feature is still experimental and incomplete at the moment. @@ -1956,7 +2438,7 @@ GENERATE_AUTOGEN_DEF = NO GENERATE_PERLMOD = NO -# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary +# If the PERLMOD_LATEX tag is set to YES, Doxygen will generate the necessary # Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI # output from the Perl module output. # The default value is: NO. @@ -1986,13 +2468,13 @@ PERLMOD_MAKEVAR_PREFIX = # Configuration options related to the preprocessor #--------------------------------------------------------------------------- -# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all +# If the ENABLE_PREPROCESSING tag is set to YES, Doxygen will evaluate all # C-preprocessor directives found in the sources and include files. # The default value is: YES. ENABLE_PREPROCESSING = YES -# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# If the MACRO_EXPANSION tag is set to YES, Doxygen will expand all macro names # in the source code. If set to NO, only conditional compilation will be # performed. Macro expansion can be done in a controlled way by setting # EXPAND_ONLY_PREDEF to YES. @@ -2018,7 +2500,8 @@ SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by the -# preprocessor. +# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of +# RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. INCLUDE_PATH = @@ -2050,7 +2533,7 @@ PREDEFINED = EXPAND_AS_DEFINED = -# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will +# If the SKIP_FUNCTION_MACROS tag is set to YES then Doxygen's preprocessor will # remove all references to function-like macros that are alone on a line, have # an all uppercase name, and do not end with a semicolon. Such function macros # are typically used for boiler-plate code, and will confuse the parser if not @@ -2074,26 +2557,26 @@ SKIP_FUNCTION_MACROS = YES # section "Linking to external documentation" for more information about the use # of tag files. # Note: Each tag file must have a unique name (where the name does NOT include -# the path). If a tag file is not located in the directory in which doxygen is +# the path). If a tag file is not located in the directory in which Doxygen is # run, you must also specify the path to the tagfile here. TAGFILES = -# When a file name is specified after GENERATE_TAGFILE, doxygen will create a +# When a file name is specified after GENERATE_TAGFILE, Doxygen will create a # tag file that is based on the input files it reads. See section "Linking to # external documentation" for more information about the usage of tag files. GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2107,102 +2590,115 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -# NO turns the diagrams off. Note that this option also works with HAVE_DOT -# disabled, but it is recommended to install and use dot, since it yields more -# powerful graphs. -# The default value is: YES. - -CLASS_DIAGRAMS = YES - -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. HIDE_UNDOC_RELATIONS = YES -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# If you set the HAVE_DOT tag to YES then Doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO -# The default value is: YES. +# The default value is: NO. ###################################################### -# not very useful at the moment. we have no complicated hierarchy. + HAVE_DOT = NO -###################################################### -# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed -# to run in parallel. When set to 0 doxygen will base this on the number of +# The DOT_NUM_THREADS specifies the number of dot invocations Doxygen is allowed +# to run in parallel. When set to 0 Doxygen will base this on the number of # processors available in the system. You can set it explicitly to a value # larger than 0 to get control over the balance between CPU load and processing # speed. # Minimum value: 0, maximum value: 32, default value: 0. # This tag requires that the tag HAVE_DOT is set to YES. +###################################################### + DOT_NUM_THREADS = 0 -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. +# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of +# subgraphs. When you want a differently looking font in the dot files that +# Doxygen generates you can specify fontname, fontcolor and fontsize attributes. +# For details please see Node, +# Edge and Graph Attributes specification You need to make sure dot is able +# to find the font, which can be done by putting it in a standard location or by +# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. Default graphviz fontsize is 14. +# The default value is: fontname=Helvetica,fontsize=10. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" + +# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can +# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. Complete documentation about +# arrows shapes. +# The default value is: labelfontname=Helvetica,labelfontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = Helvetica +DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. +# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes +# around nodes set 'shape=plain' or 'shape=plaintext' Shapes specification +# The default value is: shape=box,height=0.2,width=0.4. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTSIZE = 10 +DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" -# By default doxygen will tell dot to use the default font as specified with -# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -# the path where dot can find it using this tag. +# You can set the path where dot can find font specified with fontname in +# DOT_COMMON_ATTR and others dot attributes. # This tag requires that the tag HAVE_DOT is set to YES. DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -# each documented class showing the direct and indirect inheritance relations. -# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then Doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. Explicit enabling an inheritance +# graph or choosing a different representation for an inheritance graph of a +# specific class, can be accomplished by means of the command \inheritancegraph. +# Disabling an inheritance graph can be accomplished by means of the command +# \hideinheritancegraph. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. CLASS_GRAPH = YES -# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a +# If the COLLABORATION_GRAPH tag is set to YES then Doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES -# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. +# If the GROUP_GRAPHS tag is set to YES then Doxygen will generate a graph for +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GROUP_GRAPHS = YES -# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and +# If the UML_LOOK tag is set to YES, Doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. # The default value is: NO. @@ -2219,10 +2715,32 @@ UML_LOOK = NO # but if the number exceeds 15, the total amount of fields shown is limited to # 10. # Minimum value: 0, maximum value: 100, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. +# This tag requires that the tag UML_LOOK is set to YES. UML_LIMIT_NUM_FIELDS = 10 +# If the DOT_UML_DETAILS tag is set to NO, Doxygen will show attributes and +# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS +# tag is set to YES, Doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, Doxygen +# will not generate fields with class member information in the UML graphs. The +# class diagrams will look similar to the default class diagrams but using UML +# notation for the relationships. +# Possible values are: NO, YES and NONE. +# The default value is: NO. +# This tag requires that the tag UML_LOOK is set to YES. + +DOT_UML_DETAILS = NO + +# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters +# to display on a single line. If the actual line length exceeds this threshold +# significantly it will be wrapped across multiple lines. Some heuristics are +# applied to avoid ugly line breaks. +# Minimum value: 0, maximum value: 1000, default value: 17. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_WRAP_THRESHOLD = 17 + # If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and # collaboration graphs will show the relations between templates and their # instances. @@ -2232,24 +2750,29 @@ UML_LIMIT_NUM_FIELDS = 10 TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to -# YES then doxygen will generate a graph for each documented file showing the +# YES then Doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are -# set to YES then doxygen will generate a graph for each documented file showing +# set to YES then Doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. INCLUDED_BY_GRAPH = YES -# If the CALL_GRAPH tag is set to YES then doxygen will generate a call +# If the CALL_GRAPH tag is set to YES then Doxygen will generate a call # dependency graph for every global function or class method. # # Note that enabling this option will significantly increase the time of a run. @@ -2261,7 +2784,7 @@ INCLUDED_BY_GRAPH = YES CALL_GRAPH = NO -# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller +# If the CALLER_GRAPH tag is set to YES then Doxygen will generate a caller # dependency graph for every global function or class method. # # Note that enabling this option will significantly increase the time of a run. @@ -2273,46 +2796,59 @@ CALL_GRAPH = NO CALLER_GRAPH = NO -# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical +# If the GRAPHICAL_HIERARCHY tag is set to YES then Doxygen will graphical # hierarchy of all classes instead of a textual one. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GRAPHICAL_HIERARCHY = YES -# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the +# If the DIRECTORY_GRAPH tag is set to YES then Doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. DIRECTORY_GRAPH = YES +# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels +# of child directories generated in directory dependency graphs by dot. +# Minimum value: 1, maximum value: 25, default value: 1. +# This tag requires that the tag DIRECTORY_GRAPH is set to YES. + +DIR_GRAPH_MAX_DEPTH = 1 + # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). -# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order -# to make the SVG files visible in IE 9+ (other browsers do not have this -# requirement). -# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd, -# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo, -# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo, -# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and -# png:gdiplus:gdiplus. +# https://www.graphviz.org/)). +# +# Note the formats svg:cairo and svg:cairo:cairo cannot be used in combination +# with INTERACTIVE_SVG (the INTERACTIVE_SVG will be set to NO). +# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, +# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus, +# png:gdiplus:gdiplus, svg:cairo, svg:cairo:cairo, svg:svg, svg:svg:core, +# gif:cairo, gif:cairo:gd, gif:cairo:gdiplus, gif:gdiplus, gif:gdiplus:gdiplus, +# gif:gd, gif:gd:gd, jpg:cairo, jpg:cairo:gd, jpg:cairo:gdiplus, jpg:gd, +# jpg:gd:gd, jpg:gdiplus and jpg:gdiplus:gdiplus. # The default value is: png. # This tag requires that the tag HAVE_DOT is set to YES. DOT_IMAGE_FORMAT = png -# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to -# enable generation of interactive SVG images that allow zooming and panning. +# If DOT_IMAGE_FORMAT is set to svg or svg:svg or svg:svg:core, then this option +# can be set to YES to enable generation of interactive SVG images that allow +# zooming and panning. # # Note that this requires a modern browser other than Internet Explorer. Tested # and working are Firefox, Chrome, Safari, and Opera. -# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make -# the SVG files visible. Older versions of IE do not have SVG support. +# +# Note This option will be automatically disabled when DOT_IMAGE_FORMAT is set +# to svg:cairo or svg:cairo:cairo. # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2331,11 +2867,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in Doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2343,23 +2880,34 @@ MSCFILE_DIRS = DIAFILE_DIRS = -# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -# path where java can find the plantuml.jar file. If left blank, it is assumed -# PlantUML is not used or called during a preprocessing step. Doxygen will -# generate a warning when it encounters a \startuml command in this case and -# will not generate output for the diagram. +# When using PlantUML, the PLANTUML_JAR_PATH tag should be used to specify the +# path where java can find the plantuml.jar file or to the filename of jar file +# to be used. If left blank, it is assumed PlantUML is not used or called during +# a preprocessing step. Doxygen will generate a warning when it encounters a +# \startuml command in this case and will not generate output for the diagram. PLANTUML_JAR_PATH = -# When using plantuml, the specified paths are searched for files specified by -# the !include statement in a plantuml block. +# When using PlantUML, the PLANTUML_CFG_FILE tag can be used to specify a +# configuration file for PlantUML. + +PLANTUML_CFG_FILE = + +# When using PlantUML, the specified paths are searched for files specified by +# the !include statement in a PlantUML block. PLANTUML_INCLUDE_PATH = +# The PLANTUMLFILE_DIRS tag can be used to specify one or more directories that +# contain PlantUml files that are included in the documentation (see the +# \plantumlfile command). + +PLANTUMLFILE_DIRS = + # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes # that will be shown in the graph. If the number of nodes in a graph becomes -# larger than this value, doxygen will truncate the graph, which is visualized -# by representing a node as a red box. Note that doxygen if the number of direct +# larger than this value, Doxygen will truncate the graph, which is visualized +# by representing a node as a red box. Note that if the number of direct # children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that # the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. @@ -2380,18 +2928,6 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = NO - # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support @@ -2401,17 +2937,37 @@ DOT_TRANSPARENT = NO DOT_MULTI_TARGETS = NO -# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page +# If the GENERATE_LEGEND tag is set to YES Doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. +# Note: This tag requires that UML_LOOK isn't set, i.e. the Doxygen internal +# graphical representation for inheritance and collaboration diagrams is used. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot +# If the DOT_CLEANUP tag is set to YES, Doxygen will remove the intermediate # files that are used to generate the various graphs. +# +# Note: This setting is not only used for dot files but also for msc temporary +# files. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. DOT_CLEANUP = YES + +# You can define message sequence charts within Doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then Doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, Doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/deltachat-ffi/DoxygenLayout.xml b/deltachat-ffi/DoxygenLayout.xml index 37615cb10a..acec94e375 100644 --- a/deltachat-ffi/DoxygenLayout.xml +++ b/deltachat-ffi/DoxygenLayout.xml @@ -1,5 +1,6 @@ - - + + + @@ -11,10 +12,16 @@ + + + + + + @@ -35,4 +42,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 679b7f4e45..b2fca4d1bc 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -220,7 +220,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t; * - Strings in function arguments or return values are usually UTF-8 encoded. * * - The issue-tracker for the core library is here: - * + * * * If you need further assistance, * please do not hesitate to contact us @@ -440,17 +440,6 @@ char* dc_get_blobdir (const dc_context_t* context); * also show all mails of confirmed contacts, * DC_SHOW_EMAILS_ALL (2)= * also show mails of unconfirmed contacts (default). - * - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)= - * generate recommended key type (default), - * DC_KEY_GEN_RSA2048 (1)= - * generate RSA 2048 keypair - * DC_KEY_GEN_ED25519 (2)= - * generate Curve25519 keypair - * DC_KEY_GEN_RSA4096 (3)= - * generate RSA 4096 keypair - * - `save_mime_headers` = 1=save mime headers - * and make dc_get_mime_headers() work for subsequent calls, - * 0=do not save mime headers (default) * - `delete_device_after` = 0=do not delete messages from device automatically (default), * >=1=seconds, after which messages are deleted automatically from the device. * Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped. @@ -975,7 +964,7 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co * ~~~ * dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE); * - * dc_msg_set_file(msg, "/file/to/send.jpg", NULL); + * dc_msg_set_file_and_deduplicate(msg, "/file/to/send.jpg", NULL, NULL); * dc_send_msg(context, chat_id, msg); * * dc_msg_unref(msg); @@ -1039,6 +1028,38 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32 uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send); +/** + * Send chat members a request to edit the given message's text. + * + * Only outgoing messages sent by self can be edited. + * Edited messages should be flagged as such in the UI, see dc_msg_is_edited(). + * UI is informed about changes using the event #DC_EVENT_MSGS_CHANGED. + * If the text is not changed, no event and no edit request message are sent. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param msg_id The message ID of the message to edit. + * @param new_text The new text. + * This must not be NULL nor empty. + */ +void dc_send_edit_request (dc_context_t* context, uint32_t msg_id, const char* new_text); + + +/** + * Send chat members a request to delete the given messages. + * + * Only outgoing messages can be deleted this way + * and all messages must be in the same chat. + * No tombstone or sth. like that is left. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param msg_ids An array of uint32_t containing all message IDs to delete. + * @param msg_cnt The number of messages IDs in the msg_ids array. + */ + void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt); + + /** * Send invitation to a videochat. * @@ -1929,24 +1950,7 @@ void dc_download_full_msg (dc_context_t* context, int msg_id); /** - * Get the raw mime-headers of the given message. - * Raw headers are saved for incoming messages - * only if `dc_set_config(context, "save_mime_headers", "1")` - * was called before. - * - * @memberof dc_context_t - * @param context The context object. - * @param msg_id The message ID, must be the ID of an incoming message. - * @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage. - * Returns NULL if there are no headers saved for the given message, - * e.g. because of save_mime_headers is not set - * or the message is not incoming. - */ -char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id); - - -/** - * Delete messages. The messages are deleted on the current device and + * Delete messages. The messages are deleted on all devices and * on the IMAP server. * * @memberof dc_context_t @@ -2128,7 +2132,10 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr); -#define DC_GCL_VERIFIED_ONLY 0x01 + +// Deprecated 2025-05-20, setting this flag is a no-op. +#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01 + #define DC_GCL_ADD_SELF 0x02 @@ -2158,6 +2165,29 @@ uint32_t dc_create_contact (dc_context_t* context, const char* int dc_add_address_book (dc_context_t* context, const char* addr_book); +/** + * Make a vCard. + * + * @memberof dc_context_t + * @param context The context object. + * @param contact_id The ID of the contact to make the vCard of. + * @return vCard, must be released using dc_str_unref() after usage. + */ +char* dc_make_vcard (dc_context_t* context, uint32_t contact_id); + + +/** + * Import a vCard. + * + * @memberof dc_context_t + * @param context The context object. + * @param vcard vCard contents. + * @return Returns the IDs of the contacts in the order they appear in the vCard. + * Must be dc_array_unref()'d after usage. + */ +dc_array_t* dc_import_vcard (dc_context_t* context, const char* vcard); + + /** * Returns known and unblocked contacts. * @@ -2167,8 +2197,6 @@ int dc_add_address_book (dc_context_t* context, const char* * @param context The context object. * @param flags A combination of flags: * - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters - * - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned. - * if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned. * @param query A string to filter the list. Typically used to implement an * incremental search. NULL for no filtering. * @return An array containing all contact IDs. Must be dc_array_unref()'d @@ -2459,8 +2487,9 @@ void dc_stop_ongoing_process (dc_context_t* context); #define DC_QR_FPR_MISMATCH 220 // id=contact #define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint #define DC_QR_ACCOUNT 250 // text1=domain -#define DC_QR_BACKUP 251 +#define DC_QR_BACKUP 251 // deprecated #define DC_QR_BACKUP2 252 +#define DC_QR_BACKUP_TOO_NEW 255 #define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern #define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050") #define DC_QR_ADDR 320 // id=contact @@ -2507,11 +2536,14 @@ void dc_stop_ongoing_process (dc_context_t* context); * ask the user if they want to create an account on the given domain, * if so, call dc_set_config_from_qr() and then dc_configure(). * - * - DC_QR_BACKUP: * - DC_QR_BACKUP2: * ask the user if they want to set up a new device. * If so, pass the qr-code to dc_receive_backup(). * + * - DC_QR_BACKUP_TOO_NEW: + * show a hint to the user that this backup comes from a newer Delta Chat version + * and this device needs an update + * * - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain: * ask the user if they want to use the given service for video chats; * if so, call dc_set_config_from_qr(). @@ -4432,6 +4464,20 @@ int dc_msg_is_sent (const dc_msg_t* msg); int dc_msg_is_forwarded (const dc_msg_t* msg); +/** + * Check if the message was edited. + * + * Edited messages should be marked by the UI as such, + * e.g. by the text "Edited" beside the time. + * To edit messages, use dc_send_edit_request(). + * + * @memberof dc_msg_t + * @param msg The message object. + * @return 1=message is edited, 0=message not edited. + */ + int dc_msg_is_edited (const dc_msg_t* msg); + + /** * Check if the message is an informational message, created by the * device or by another users. Such messages are not "typed" by the user but @@ -4461,6 +4507,11 @@ int dc_msg_is_info (const dc_msg_t* msg); * UIs can display e.g. an icon based upon the type. * * Currently, the following types are defined: + * - DC_INFO_GROUP_NAME_CHANGED (2) - "Group name changd from OLD to BY by CONTACT" + * - DC_INFO_GROUP_IMAGE_CHANGED (3) - "Group image changd by CONTACT" + * - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT" + * - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT" + * - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT" * - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected" * - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected" * - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet", @@ -4468,6 +4519,10 @@ int dc_msg_is_info (const dc_msg_t* msg); * and also offer a way to fix the encryption, eg. by a button offering a QR scan * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` * + * For the messages that refer to a CONTACT, + * dc_msg_get_info_contact_id() returns the contact ID. + * The UI should open the contact's profile when tapping the info message. + * * Even when you display an icon, * you should still display the text of the informational message using dc_msg_get_text() * @@ -4480,6 +4535,29 @@ int dc_msg_is_info (const dc_msg_t* msg); int dc_msg_get_info_type (const dc_msg_t* msg); +/** + * Return the contact ID of the profile to open when tapping the info message. + * + * - For DC_INFO_MEMBER_ADDED_TO_GROUP and DC_INFO_MEMBER_REMOVED_FROM_GROUP, + * this is the contact being added/removed. + * The contact that did the adding/removal is usually only a tap away + * (as introducer and/or atop of the memberlist), + * and usually more known anyways. + * - For DC_INFO_GROUP_NAME_CHANGED, DC_INFO_GROUP_IMAGE_CHANGED and DC_INFO_EPHEMERAL_TIMER_CHANGED + * this is the contact who did the change. + * + * No need to check additionally for dc_msg_get_info_type(), + * unless you e.g. want to show the info message in another style. + * + * @memberof dc_msg_t + * @param msg The message object. + * @return If the info message refers to a contact, + * this contact ID or DC_CONTACT_ID_SELF is returned. + * Otherwise 0. + */ +uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg); + + // DC_INFO* uses the same values as SystemMessage in rust-land #define DC_INFO_UNKNOWN 0 #define DC_INFO_GROUP_NAME_CHANGED 2 @@ -4741,22 +4819,6 @@ void dc_msg_set_subject (dc_msg_t* msg, const char* subjec void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name); -/** - * Set the file associated with a message object. - * This does not alter any information in the database - * nor copy or move the file or checks if the file exist. - * All this can be done with dc_send_msg() later. - * - * @memberof dc_msg_t - * @param msg The message object. - * @param file If the message object is used in dc_send_msg() later, - * this must be the full path of the image file to send. - * @param filemime The MIME type of the file. NULL if you don't know or don't care. - * @deprecated 2025-01-21 Use dc_msg_set_file_and_deduplicate instead - */ -void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime); - - /** * Sets the file associated with a message. * @@ -4784,7 +4846,7 @@ void dc_msg_set_file_and_deduplicate(dc_msg_t* msg, const char* file, /** * Set the dimensions associated with message object. - * Typically this is the width and the height of an image or video associated using dc_msg_set_file(). + * Typically this is the width and the height of an image or video associated using dc_msg_set_file_and_deduplicate(). * This does not alter any information in the database; this may be done by dc_send_msg() later. * * @memberof dc_msg_t @@ -4797,7 +4859,7 @@ void dc_msg_set_dimension (dc_msg_t* msg, int width, int hei /** * Set the duration associated with message object. - * Typically this is the duration of an audio or video associated using dc_msg_set_file(). + * Typically this is the duration of an audio or video associated using dc_msg_set_file_and_deduplicate(). * This does not alter any information in the database; this may be done by dc_send_msg() later. * * @memberof dc_msg_t @@ -4930,6 +4992,7 @@ dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg); * Can be used by UI to show a button to go the original message * and an option to "Unsave" the message. * + * @memberof dc_msg_t * @param msg The message object. Usually, this refers to a a message inside "Saved Messages". * @return The message ID of the original message. * 0 if the given message object is not a "Saved Message" @@ -4944,6 +5007,7 @@ uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg); * Deleting the returned message will un-save the message. * The state "is saved" can be used to show some icon to indicate that a message was saved. * + * @memberof dc_msg_t * @param msg The message object. Usually, this refers to a a message outside "Saved Messages". * @return The message ID inside "Saved Messages", if any. * 0 if the given message object is not saved. @@ -5434,7 +5498,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); * If you want to define the type of a dc_msg_t object for sending, * use dc_msg_new(). * Depending on the type, you will set more properties using e.g. - * dc_msg_set_text() or dc_msg_set_file(). + * dc_msg_set_text() or dc_msg_set_file_and_deduplicate(). * To finally send the message, use dc_send_msg(). * * To get the types of dc_msg_t objects received, use dc_msg_get_viewtype(). @@ -5455,7 +5519,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * Image message. * If the image is an animated GIF, the type #DC_MSG_GIF should be used. - * File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension() + * File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension() * and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height(). * * Before sending, the image is recoded to an reasonable size, @@ -5468,7 +5532,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * Animated GIF message. - * File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension() + * File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension() * and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height(). */ #define DC_MSG_GIF 21 @@ -5486,7 +5550,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * Message containing an audio file. - * File and duration are set via dc_msg_set_file(), dc_msg_set_duration() + * File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration() * and retrieved via dc_msg_get_file(), and dc_msg_get_duration(). */ #define DC_MSG_AUDIO 40 @@ -5495,7 +5559,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * A voice message that was directly recorded by the user. * For all other audio messages, the type #DC_MSG_AUDIO should be used. - * File and duration are set via dc_msg_set_file(), dc_msg_set_duration() + * File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration() * and retrieved via dc_msg_get_file(), and dc_msg_get_duration(). */ #define DC_MSG_VOICE 41 @@ -5504,7 +5568,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * Video messages. * File, width, height, and duration - * are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration() + * are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension(), dc_msg_set_duration() * and retrieved via * dc_msg_get_file(), dc_msg_get_width(), * dc_msg_get_height(), and dc_msg_get_duration(). @@ -5514,7 +5578,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); /** * Message containing any file, e.g. a PDF. - * The file is set via dc_msg_set_file() + * The file is set via dc_msg_set_file_and_deduplicate() * and retrieved via dc_msg_get_file(). */ #define DC_MSG_FILE 60 @@ -6278,6 +6342,18 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021 +/** + * Chat was deleted. + * This event is emitted in response to dc_delete_chat() + * called on this or another device. + * The event is a good place to remove notifications or homescreen shortcuts. + * + * @param data1 (int) chat_id + * @param data2 (int) 0 + */ +#define DC_EVENT_CHAT_DELETED 2023 + + /** * Contact(s) created, renamed, verified, blocked or deleted. * @@ -6518,15 +6594,6 @@ void dc_event_unref(dc_event_t* event); #define DC_MEDIA_QUALITY_WORSE 1 -/* - * Values for dc_get|set_config("key_gen_type") - */ -#define DC_KEY_GEN_DEFAULT 0 -#define DC_KEY_GEN_RSA2048 1 -#define DC_KEY_GEN_ED25519 2 -#define DC_KEY_GEN_RSA4096 3 - - /** * @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS * @@ -6846,12 +6913,12 @@ void dc_event_unref(dc_event_t* event); /// "Autocrypt Setup Message" /// -/// Used in subjects of outgoing Autocrypt Setup Messages. +/// @deprecated 2025-04 #define DC_STR_AC_SETUP_MSG_SUBJECT 42 /// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup" /// -/// Used as message text of outgoing Autocrypt Setup Messages. +/// @deprecated 2025-04 #define DC_STR_AC_SETUP_MSG_BODY 43 /// "Cannot login as %1$s." @@ -7526,9 +7593,14 @@ void dc_event_unref(dc_event_t* event); /// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message." /// -/// Used as info message. +/// @deprecated 2025-03 #define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191 +/// "The contact must be online to proceed. This process will continue automatically in background." +/// +/// Used as info message. +#define DC_STR_SECUREJOIN_TAKES_LONGER 192 + /// "Contact". Deprecated, currently unused. #define DC_STR_CONTACT 200 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 33456ef56e..5ad6f92867 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -18,7 +18,7 @@ use std::future::Future; use std::ops::Deref; use std::ptr; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, SystemTime}; use anyhow::Context as _; @@ -37,8 +37,8 @@ use deltachat::*; use deltachat::{accounts::Accounts, log::LogExt}; use deltachat_jsonrpc::api::CommandApi; use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; +use message::Viewtype; use num_traits::{FromPrimitive, ToPrimitive}; -use once_cell::sync::Lazy; use rand::Rng; use tokio::runtime::Runtime; use tokio::sync::RwLock; @@ -68,7 +68,8 @@ const DC_GCM_INFO_ONLY: u32 = 0x02; /// Struct representing the deltachat context. pub type dc_context_t = Context; -static RT: Lazy = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime")); +static RT: LazyLock = + LazyLock::new(|| Runtime::new().expect("unable to create tokio runtime")); fn block_on(fut: T) -> T::Output where @@ -536,7 +537,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::IncomingReaction { .. } => 2002, EventType::IncomingWebxdcNotify { .. } => 2003, EventType::IncomingMsg { .. } => 2005, - EventType::IncomingMsgBunch { .. } => 2006, + EventType::IncomingMsgBunch => 2006, EventType::MsgsNoticed { .. } => 2008, EventType::MsgDelivered { .. } => 2010, EventType::MsgFailed { .. } => 2012, @@ -544,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::MsgDeleted { .. } => 2016, EventType::ChatModified(_) => 2020, EventType::ChatEphemeralTimerModified { .. } => 2021, + EventType::ChatDeleted { .. } => 2023, EventType::ContactsChanged(_) => 2030, EventType::LocationChanged(_) => 2035, EventType::ConfigureProgress { .. } => 2041, @@ -593,7 +595,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: | EventType::ConnectivityChanged | EventType::SelfavatarChanged | EventType::ConfigSynced { .. } - | EventType::IncomingMsgBunch { .. } + | EventType::IncomingMsgBunch | EventType::ErrorSelfNotInGroup(_) | EventType::AccountsBackgroundFetchDone | EventType::ChatlistChanged @@ -610,7 +612,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: | EventType::MsgRead { chat_id, .. } | EventType::MsgDeleted { chat_id, .. } | EventType::ChatModified(chat_id) - | EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int, + | EventType::ChatEphemeralTimerModified { chat_id, .. } + | EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int, EventType::ContactsChanged(id) | EventType::LocationChanged(id) => { let id = id.unwrap_or_default(); id.to_u32() as libc::c_int @@ -667,7 +670,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::MsgsNoticed(_) | EventType::ConnectivityChanged | EventType::WebxdcInstanceDeleted { .. } - | EventType::IncomingMsgBunch { .. } + | EventType::IncomingMsgBunch | EventType::SelfavatarChanged | EventType::AccountsBackgroundFetchDone | EventType::ChatlistChanged @@ -676,6 +679,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::AccountsItemChanged | EventType::ConfigSynced { .. } | EventType::ChatModified(_) + | EventType::ChatDeleted { .. } | EventType::WebxdcRealtimeAdvertisementReceived { .. } | EventType::EventChannelOverflow { .. } => 0, EventType::MsgsChanged { msg_id, .. } @@ -767,7 +771,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::WebxdcInstanceDeleted { .. } | EventType::AccountsBackgroundFetchDone | EventType::ChatEphemeralTimerModified { .. } - | EventType::IncomingMsgBunch { .. } + | EventType::ChatDeleted { .. } + | EventType::IncomingMsgBunch | EventType::ChatlistItemChanged { .. } | EventType::ChatlistChanged | EventType::AccountsChanged @@ -1041,6 +1046,42 @@ pub unsafe extern "C" fn dc_send_text_msg( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_send_edit_request( + context: *mut dc_context_t, + msg_id: u32, + new_text: *const libc::c_char, +) { + if context.is_null() || new_text.is_null() { + eprintln!("ignoring careless call to dc_send_edit_request()"); + return; + } + let ctx = &*context; + let new_text = to_string_lossy(new_text); + + block_on(chat::send_edit_request(ctx, MsgId::new(msg_id), new_text)) + .unwrap_or_log_default(ctx, "Failed to send text edit") +} + +#[no_mangle] +pub unsafe extern "C" fn dc_send_delete_request( + context: *mut dc_context_t, + msg_ids: *const u32, + msg_cnt: libc::c_int, +) { + if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 { + eprintln!("ignoring careless call to dc_send_delete_request()"); + return; + } + let ctx = &*context; + let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt); + + block_on(message::delete_msgs_ex(ctx, &msg_ids, true)) + .context("failed dc_send_delete_request() call") + .log_err(ctx) + .ok(); +} + #[no_mangle] pub unsafe extern "C" fn dc_send_videochat_invitation( context: *mut dc_context_t, @@ -1618,6 +1659,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) - return ptr::null_mut(); } let ctx = &*context; + let context: Context = ctx.clone(); block_on(async move { match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await { @@ -1913,28 +1955,6 @@ pub unsafe extern "C" fn dc_get_msg_html( .strdup() } -#[no_mangle] -pub unsafe extern "C" fn dc_get_mime_headers( - context: *mut dc_context_t, - msg_id: u32, -) -> *mut libc::c_char { - if context.is_null() { - eprintln!("ignoring careless call to dc_get_mime_headers()"); - return ptr::null_mut(); // NULL explicitly defined as "no mime headers" - } - let ctx = &*context; - - block_on(async move { - let mime = message::get_mime_headers(ctx, MsgId::new(msg_id)) - .await - .unwrap_or_log_default(ctx, "failed to get mime headers"); - if mime.is_empty() { - return ptr::null_mut(); - } - mime.strdup() - }) -} - #[no_mangle] pub unsafe extern "C" fn dc_delete_msgs( context: *mut dc_context_t, @@ -2057,7 +2077,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) -> ctx, "dc_get_msg called with special msg_id={msg_id}, returning empty msg" ); - message::Message::default() + message::Message::new(Viewtype::default()) } else { warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}"); return ptr::null_mut(); @@ -2151,6 +2171,48 @@ pub unsafe extern "C" fn dc_add_address_book( }) } +#[no_mangle] +pub unsafe extern "C" fn dc_make_vcard( + context: *mut dc_context_t, + contact_id: u32, +) -> *mut libc::c_char { + if context.is_null() { + eprintln!("ignoring careless call to dc_make_vcard()"); + return ptr::null_mut(); + } + let ctx = &*context; + let contact_id = ContactId::new(contact_id); + + block_on(contact::make_vcard(ctx, &[contact_id])) + .unwrap_or_log_default(ctx, "dc_make_vcard failed") + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_import_vcard( + context: *mut dc_context_t, + vcard: *const libc::c_char, +) -> *mut dc_array::dc_array_t { + if context.is_null() || vcard.is_null() { + eprintln!("ignoring careless call to dc_import_vcard()"); + return ptr::null_mut(); + } + let ctx = &*context; + + match block_on(contact::import_vcard(ctx, &to_string_lossy(vcard))) + .context("dc_import_vcard failed") + .log_err(ctx) + { + Ok(contact_ids) => Box::into_raw(Box::new(dc_array_t::from( + contact_ids + .iter() + .map(|id| id.to_u32()) + .collect::>(), + ))), + Err(_) => ptr::null_mut(), + } +} + #[no_mangle] pub unsafe extern "C" fn dc_get_contacts( context: *mut dc_context_t, @@ -2964,7 +3026,7 @@ pub unsafe extern "C" fn dc_chatlist_get_context( /// context, but the Rust API does not, so the FFI layer needs to glue /// these together. pub struct ChatWrapper { - context: *const dc_context_t, + context: Context, chat: chat::Chat, } @@ -3031,14 +3093,13 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut return ptr::null_mut(); // NULL explicitly defined as "no image" } let ffi_chat = &*chat; - let ctx = &*ffi_chat.context; block_on(async move { - match ffi_chat.chat.get_profile_image(ctx).await { + match ffi_chat.chat.get_profile_image(&ffi_chat.context).await { Ok(Some(p)) => p.to_string_lossy().strdup(), Ok(None) => ptr::null_mut(), Err(err) => { - error!(ctx, "failed to get profile image: {err:#}"); + error!(ffi_chat.context, "failed to get profile image: {err:#}"); ptr::null_mut() } } @@ -3052,9 +3113,9 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 { return 0; } let ffi_chat = &*chat; - let ctx = &*ffi_chat.context; - block_on(ffi_chat.chat.get_color(ctx)).unwrap_or_log_default(ctx, "Failed get_color") + block_on(ffi_chat.chat.get_color(&ffi_chat.context)) + .unwrap_or_log_default(&ffi_chat.context, "Failed get_color") } #[no_mangle] @@ -3118,10 +3179,9 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int { return 0; } let ffi_chat = &*chat; - let ctx = &*ffi_chat.context; - block_on(ffi_chat.chat.can_send(ctx)) + block_on(ffi_chat.chat.can_send(&ffi_chat.context)) .context("can_send failed") - .log_err(ctx) + .log_err(&ffi_chat.context) .unwrap_or_default() as libc::c_int } @@ -3683,6 +3743,16 @@ pub unsafe extern "C" fn dc_msg_is_forwarded(msg: *mut dc_msg_t) -> libc::c_int ffi_msg.message.is_forwarded().into() } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_is_edited(msg: *mut dc_msg_t) -> libc::c_int { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_is_edited()"); + return 0; + } + let ffi_msg = &*msg; + ffi_msg.message.is_edited().into() +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_is_info(msg: *mut dc_msg_t) -> libc::c_int { if msg.is_null() { @@ -3703,6 +3773,20 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int ffi_msg.message.get_info_type() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_info_contact_id(msg: *mut dc_msg_t) -> u32 { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_get_info_contact_id()"); + return 0; + } + let ffi_msg = &*msg; + let context = &*ffi_msg.context; + block_on(ffi_msg.message.get_info_contact_id(context)) + .unwrap_or_default() + .map(|id| id.to_u32()) + .unwrap_or_default() +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char { if msg.is_null() { @@ -3818,23 +3902,6 @@ pub unsafe extern "C" fn dc_msg_set_override_sender_name( .set_override_sender_name(to_opt_string_lossy(name)) } -#[no_mangle] -pub unsafe extern "C" fn dc_msg_set_file( - msg: *mut dc_msg_t, - file: *const libc::c_char, - filemime: *const libc::c_char, -) { - if msg.is_null() || file.is_null() { - eprintln!("ignoring careless call to dc_msg_set_file()"); - return; - } - let ffi_msg = &mut *msg; - ffi_msg.message.set_file( - to_string_lossy(file), - to_opt_string_lossy(filemime).as_deref(), - ) -} - #[no_mangle] pub unsafe extern "C" fn dc_msg_set_file_and_deduplicate( msg: *mut dc_msg_t, diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 6c283404b6..0649b4ec21 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -50,6 +50,7 @@ impl Lot { Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)), Qr::Account { domain } => Some(Cow::Borrowed(domain)), Qr::Backup2 { .. } => None, + Qr::BackupTooNew { .. } => None, Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)), Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))), Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed), @@ -103,6 +104,7 @@ impl Lot { Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr, Qr::Account { .. } => LotState::QrAccount, Qr::Backup2 { .. } => LotState::QrBackup2, + Qr::BackupTooNew { .. } => LotState::QrBackupTooNew, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, Qr::Proxy { .. } => LotState::QrProxy, Qr::Addr { .. } => LotState::QrAddr, @@ -129,6 +131,7 @@ impl Lot { Qr::FprWithoutAddr { .. } => Default::default(), Qr::Account { .. } => Default::default(), Qr::Backup2 { .. } => Default::default(), + Qr::BackupTooNew { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), Qr::Proxy { .. } => Default::default(), Qr::Addr { contact_id, .. } => contact_id.to_u32(), @@ -178,10 +181,10 @@ pub enum LotState { /// text1=domain QrAccount = 250, - QrBackup = 251, - QrBackup2 = 252, + QrBackupTooNew = 255, + /// text1=domain, text2=instance pattern QrWebrtcInstance = 260, diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 2e8ce2c9ba..89d9944bbd 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,28 +1,19 @@ [package] name = "deltachat-jsonrpc" -version = "1.155.2" +version = "1.159.5" description = "DeltaChat JSON-RPC API" edition = "2021" -default-run = "deltachat-jsonrpc-server" license = "MPL-2.0" -repository = "https://github.com/deltachat/deltachat-core-rust" - -[[bin]] -name = "deltachat-jsonrpc-server" -path = "src/webserver.rs" -required-features = ["webserver"] +repository = "https://github.com/chatmail/core" [dependencies] anyhow = { workspace = true } deltachat = { workspace = true } deltachat-contact-tools = { workspace = true } num-traits = { workspace = true } -schemars = "0.8.21" +schemars = "0.8.22" serde = { workspace = true, features = ["derive"] } -tempfile = { workspace = true } -log = { workspace = true } async-channel = { workspace = true } -futures = { workspace = true } serde_json = { workspace = true } yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] } typescript-type-def = { version = "0.5.13", features = ["json_value"] } @@ -31,15 +22,12 @@ sanitize-filename = { workspace = true } walkdir = "2.5.0" base64 = { workspace = true } -# optional dependencies -axum = { version = "0.7", optional = true, features = ["ws"] } -env_logger = { version = "0.11.6", optional = true } - [dev-dependencies] tokio = { workspace = true, features = ["full", "rt-multi-thread"] } +tempfile = { workspace = true } +futures = { workspace = true } [features] default = ["vendored"] -webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"] vendored = ["deltachat/vendored"] diff --git a/deltachat-jsonrpc/README.md b/deltachat-jsonrpc/README.md index 7e8110652f..fc02bd4967 100644 --- a/deltachat-jsonrpc/README.md +++ b/deltachat-jsonrpc/README.md @@ -4,46 +4,16 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte The JSON-RPC API is exposed in two fashions: -* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost. -* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details. +* A executable `deltachat-rpc-server` that exposes the JSON-RPC API through stdio. +* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). It exposes the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details. -We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details. +We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. ## Usage -#### Running the WebSocket server - -From within this folder, you can start the WebSocket server with the following command: - -```sh -cargo run --features webserver -``` - -If you want to use the server in a production setup, first build it in release mode: - -```sh -cargo build --features webserver --release -``` -You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder. - -The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started. - -The server can be configured with environment variables: - -|variable|default|description| -|-|-|-| -|`DC_PORT`|`20808`|port to listen on| -|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory| - -If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross): - -```sh -cross build --features=webserver --target armv7-linux-androideabi --release -``` - #### Using the TypeScript/JavaScript client -The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder. +The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder. To use it locally, first install the dependencies and compile the TypeScript code to JavaScript: ```sh @@ -52,15 +22,7 @@ npm install npm run build ``` -The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class. - - -```typescript -import { DeltaChat } from './deltachat.bundle.js' -const dc = new DeltaChat('ws://localhost:20808/ws') -const accounts = await dc.rpc.getAllAccounts() -console.log('accounts', accounts) -``` +The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client). A script is included to build autogenerated documentation, which includes all RPC methods: ```sh @@ -73,18 +35,6 @@ Then open the [`typescript/docs`](typescript/docs) folder in a web browser. #### Running the example app -We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this. - -```sh -cd typescript -npm run build -npm run example:build -npm run example:start -``` -Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser. - -Run `npm run example:dev` to live-rebuild the example app when files changes. - ### Testing The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client. @@ -104,14 +54,12 @@ cd typescript npm run test ``` -This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server. - -The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm). +This will build the `deltachat-jsonrpc-server` binary and then run a test suite. -Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain. +The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain. ``` -CHATMAIL_DOMAIN=chat.example.org npm run test +CHATMAIL_DOMAIN=ci-chatmail.testrun.org npm run test ``` #### Test Coverage diff --git a/deltachat-jsonrpc/TODO.md b/deltachat-jsonrpc/TODO.md deleted file mode 100644 index c09079a017..0000000000 --- a/deltachat-jsonrpc/TODO.md +++ /dev/null @@ -1,28 +0,0 @@ -# TODO - -- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer - -## MVP - Websocket server&client - -For kaiOS and other experiments, like a deltachat "web" over network from an android phone. - -- [ ] coverage for a majority of the API -- [ ] Blobs served -- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on) -- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node -- [ ] Web push API? At least some kind of notification hook closure this lib can accept. - -### Other Ideas for the Websocket server - -- [ ] make sure there can only be one connection at a time to the ws - - why? , it could give problems if its commanded from multiple connections -- [ ] encrypted connection? -- [ ] authenticated connection? -- [ ] Look into unit-testing for the proc macros? -- [ ] proc macro taking over doc comments to generated typescript file - -## Desktop Apis - -Incomplete todo for desktop api porting, just some remainders for points that might need more work: - -- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 5c34a554c9..bdab3bba6b 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str; use std::sync::Arc; use std::time::Duration; @@ -7,6 +7,7 @@ use std::{collections::HashMap, str::FromStr}; use anyhow::{anyhow, bail, ensure, Context, Result}; pub use deltachat::accounts::Accounts; +use deltachat::blob::BlobObject; use deltachat::chat::{ self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, @@ -21,7 +22,7 @@ use deltachat::ephemeral::Timer; use deltachat::location; use deltachat::message::get_msg_read_receipts; use deltachat::message::{ - self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype, + self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype, }; use deltachat::peer_channels::{ leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data, @@ -38,6 +39,7 @@ use deltachat::{imex, info}; use sanitize_filename::is_sanitized; use tokio::fs; use tokio::sync::{watch, Mutex, RwLock}; +use types::login_param::EnteredLoginParam; use walkdir::WalkDir; use yerpc::rpc; @@ -225,8 +227,9 @@ impl CommandApi { /// Get a list of all configured accounts. async fn get_all_accounts(&self) -> Result> { let mut accounts = Vec::new(); - for id in self.accounts.read().await.get_all() { - let context_option = self.accounts.read().await.get_account(id); + let accounts_lock = self.accounts.read().await; + for id in accounts_lock.get_all() { + let context_option = accounts_lock.get_account(id); if let Some(ctx) = context_option { accounts.push(Account::from_context(&ctx, id).await?) } @@ -324,8 +327,12 @@ impl CommandApi { .get_config_bool(deltachat::config::Config::ProxyEnabled) .await?; - let provider_info = - get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await; + let provider_info = get_provider_info( + &ctx, + email.split('@').next_back().unwrap_or(""), + proxy_enabled, + ) + .await; Ok(ProviderInfo::from_dc_type(provider_info)) } @@ -341,11 +348,19 @@ impl CommandApi { ctx.get_info().await } + /// Get the blob dir. async fn get_blob_dir(&self, account_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned())) } + /// Copy file to blob dir. + async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result { + let ctx = self.get_context(account_id).await?; + let file = Path::new(&path); + Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path()) + } + async fn draft_self_report(&self, account_id: u32) -> Result { let ctx = self.get_context(account_id).await?; Ok(ctx.draft_self_report().await?.to_u32()) @@ -422,6 +437,9 @@ impl CommandApi { /// Configures this account with the currently set parameters. /// Setup the credential config before calling this. + /// + /// Deprecated as of 2025-02; use `add_transport_from_qr()` + /// or `add_or_update_transport()` instead. async fn configure(&self, account_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; ctx.stop_io().await; @@ -436,6 +454,78 @@ impl CommandApi { Ok(()) } + /// Configures a new email account using the provided parameters + /// and adds it as a transport. + /// + /// If the email address is the same as an existing transport, + /// then this existing account will be reconfigured instead of a new one being added. + /// + /// This function stops and starts IO as needed. + /// + /// Usually it will be enough to only set `addr` and `password`, + /// and all the other settings will be autoconfigured. + /// + /// During configuration, ConfigureProgress events are emitted; + /// they indicate a successful configuration as well as errors + /// and may be used to create a progress bar. + /// This function will return after configuration is finished. + /// + /// If configuration is successful, + /// the working server parameters will be saved + /// and used for connecting to the server. + /// The parameters entered by the user will be saved separately + /// so that they can be prefilled when the user opens the server-configuration screen again. + /// + /// See also: + /// - [Self::is_configured()] to check whether there is + /// at least one working transport. + /// - [Self::add_transport_from_qr()] to add a transport + /// from a server encoded in a QR code. + /// - [Self::list_transports()] to get a list of all configured transports. + /// - [Self::delete_transport()] to remove a transport. + async fn add_or_update_transport( + &self, + account_id: u32, + param: EnteredLoginParam, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.add_or_update_transport(&mut param.try_into()?).await + } + + /// Deprecated 2025-04. Alias for [Self::add_or_update_transport()]. + async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> { + self.add_or_update_transport(account_id, param).await + } + + /// Adds a new email account as a transport + /// using the server encoded in the QR code. + /// See [Self::add_or_update_transport]. + async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.add_transport_from_qr(&qr).await + } + + /// Returns the list of all email accounts that are used as a transport in the current profile. + /// Use [Self::add_or_update_transport()] to add or change a transport + /// and [Self::delete_transport()] to delete a transport. + async fn list_transports(&self, account_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let res = ctx + .list_transports() + .await? + .into_iter() + .map(|t| t.into()) + .collect(); + Ok(res) + } + + /// Removes the transport with the specified email address + /// (i.e. [EnteredLoginParam::addr]). + async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.delete_transport(&addr).await + } + /// Signal an ongoing process to stop. async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; @@ -1205,7 +1295,15 @@ impl CommandApi { async fn delete_messages(&self, account_id: u32, message_ids: Vec) -> Result<()> { let ctx = self.get_context(account_id).await?; let msgs: Vec = message_ids.into_iter().map(MsgId::new).collect(); - delete_msgs(&ctx, &msgs).await + delete_msgs_ex(&ctx, &msgs, false).await + } + + /// Delete messages. The messages are deleted on the current device, + /// on the IMAP server and also for all chat members + async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let msgs: Vec = message_ids.into_iter().map(MsgId::new).collect(); + delete_msgs_ex(&ctx, &msgs, true).await } /// Get an informational text for a single message. The text is multiline and may @@ -1305,6 +1403,12 @@ impl CommandApi { Ok(results) } + async fn save_msgs(&self, account_id: u32, message_ids: Vec) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let message_ids: Vec = message_ids.into_iter().map(MsgId::new).collect(); + chat::save_msgs(&ctx, &message_ids).await + } + // --------------------------------------------- // contact // --------------------------------------------- @@ -1447,6 +1551,7 @@ impl CommandApi { Ok(()) } + /// Sets display name for existing contact. async fn change_contact_name( &self, account_id: u32, @@ -1455,9 +1560,7 @@ impl CommandApi { ) -> Result<()> { let ctx = self.get_context(account_id).await?; let contact_id = ContactId::new(contact_id); - let contact = Contact::get_by_id(&ctx, contact_id).await?; - let addr = contact.get_addr(); - Contact::create(&ctx, &name, addr).await?; + contact_id.set_name(&ctx, &name).await?; Ok(()) } @@ -1512,6 +1615,18 @@ impl CommandApi { .collect()) } + /// Imports contacts from a vCard. + /// + /// Returns the ids of created/modified contacts in the order they appear in the vCard. + async fn import_vcard_contents(&self, account_id: u32, vcard: String) -> Result> { + let ctx = self.get_context(account_id).await?; + Ok(deltachat::contact::import_vcard(&ctx, &vcard) + .await? + .into_iter() + .map(|c| c.to_u32()) + .collect()) + } + /// Returns a vCard containing contacts with the given ids. async fn make_vcard(&self, account_id: u32, contacts: Vec) -> Result { let ctx = self.get_context(account_id).await?; @@ -1845,13 +1960,9 @@ impl CommandApi { /// Get href from a WebxdcInfoMessage which might include a hash holding /// information about a specific position or state in a webxdc app (optional) - async fn get_webxdc_href( - &self, - account_id: u32, - instance_msg_id: u32, - ) -> Result> { + async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; - let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?; + let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?; Ok(message.get_webxdc_href()) } @@ -1944,7 +2055,7 @@ impl CommandApi { let ctx = self.get_context(account_id).await?; let mut msg = Message::new(Viewtype::Sticker); - msg.set_file(&sticker_path, None); + msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?; // JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image] msg.force_sticker(); @@ -1998,6 +2109,16 @@ impl CommandApi { Ok(msg_id) } + async fn send_edit_request( + &self, + account_id: u32, + msg_id: u32, + new_text: String, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + chat::send_edit_request(&ctx, MsgId::new(msg_id), new_text).await + } + /// Checks if messages can be sent to a given chat. async fn can_send(&self, account_id: u32, chat_id: u32) -> Result { let ctx = self.get_context(account_id).await?; @@ -2164,12 +2285,45 @@ impl CommandApi { // mimics the old desktop call, will get replaced with something better in the composer rewrite, // the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map + /// Send a message to a chat. + /// + /// This function returns after the message has been placed in the sending queue. + /// This does not imply that the message was really sent out yet. + /// However, from your view, you're done with the message. + /// Sooner or later it will find its way. + /// + /// **Attaching files:** + /// + /// Pass the file path in the `file` parameter. + /// If `file` is not in the blob directory yet, + /// it will be copied into the blob directory. + /// If you want, you can delete the file immediately after this function returns. + /// + /// You can also write the attachment directly into the blob directory + /// and then pass the path as the `file` parameter; + /// this will prevent an unnecessary copying of the file. + /// + /// In `filename`, you can pass the original name of the file, + /// which will then be shown in the UI. + /// in this case the current name of `file` on the filesystem will be ignored. + /// + /// In order to deduplicate files that contain the same data, + /// the file will be named `.`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`. + /// + /// NOTE: + /// - This function will rename the file. To get the new file path, call `get_file()`. + /// - The file must not be modified after this function was called. + /// - Images etc. will NOT be recoded. + /// In order to recode images, + /// use `misc_set_draft` and pass `Image` as the viewtype. + #[expect(clippy::too_many_arguments)] async fn misc_send_msg( &self, account_id: u32, chat_id: u32, text: Option, file: Option, + filename: Option, location: Option<(f64, f64)>, quoted_message_id: Option, ) -> Result<(u32, MessageObject)> { @@ -2181,7 +2335,7 @@ impl CommandApi { }); message.set_text(text.unwrap_or_default()); if let Some(file) = file { - message.set_file(file, None); + message.set_file_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?; } if let Some((latitude, longitude)) = location { message.set_location(latitude, longitude); @@ -2209,12 +2363,14 @@ impl CommandApi { // the better version should support: // - changing viewtype to enable/disable compression // - keeping same message id as long as attachment does not change for webxdc messages + #[expect(clippy::too_many_arguments)] async fn misc_set_draft( &self, account_id: u32, chat_id: u32, text: Option, file: Option, + filename: Option, quoted_message_id: Option, view_type: Option, ) -> Result<()> { @@ -2231,7 +2387,7 @@ impl CommandApi { )); draft.set_text(text.unwrap_or_default()); if let Some(file) = file { - draft.set_file(file, None); + draft.set_file_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?; } if let Some(id) = quoted_message_id { draft diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 918ff7c1c6..7472b23025 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -84,45 +84,78 @@ pub enum EventType { /// - Messages sent, received or removed /// - Chats created, deleted or archived /// - A draft has been set - /// - /// `chatId` is set if only a single chat is affected by the changes, otherwise 0. - /// `msgId` is set if only a single message is affected by the changes, otherwise 0. #[serde(rename_all = "camelCase")] - MsgsChanged { chat_id: u32, msg_id: u32 }, + MsgsChanged { + /// Set if only a single chat is affected by the changes, otherwise 0. + chat_id: u32, + + /// Set if only a single message is affected by the changes, otherwise 0. + msg_id: u32, + }, /// Reactions for the message changed. #[serde(rename_all = "camelCase")] ReactionsChanged { + /// ID of the chat which the message belongs to. chat_id: u32, + + /// ID of the message for which reactions were changed. msg_id: u32, + + /// ID of the contact whose reaction set is changed. contact_id: u32, }, - /// Incoming reaction, should be notified. + /// A reaction to one's own sent message received. + /// Typically, the UI will show a notification for that. + /// + /// In addition to this event, ReactionsChanged is emitted. #[serde(rename_all = "camelCase")] IncomingReaction { + /// ID of the chat which the message belongs to. chat_id: u32, + + /// ID of the contact whose reaction set is changed. contact_id: u32, + + /// ID of the message for which reactions were changed. msg_id: u32, + + /// The reaction. reaction: String, }, /// Incoming webxdc info or summary update, should be notified. #[serde(rename_all = "camelCase")] IncomingWebxdcNotify { + /// ID of the chat. chat_id: u32, + + /// ID of the contact sending. contact_id: u32, + + /// ID of the added info message or webxdc instance in case of summary change. msg_id: u32, + + /// Text to notify. text: String, + + /// Link assigned to this notification, if any. href: Option, }, - /// There is a fresh message. Typically, the user will show an notification + /// There is a fresh message. Typically, the user will show a notification /// when receiving this message. /// /// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event. #[serde(rename_all = "camelCase")] - IncomingMsg { chat_id: u32, msg_id: u32 }, + IncomingMsg { + /// ID of the chat where the message is assigned. + chat_id: u32, + + /// ID of the message. + msg_id: u32, + }, /// Downloading a bunch of messages just finished. This is an /// event to allow the UI to only show one notification per message bunch, @@ -138,21 +171,57 @@ pub enum EventType { /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// DC_STATE_OUT_DELIVERED, see `Message.state`. #[serde(rename_all = "camelCase")] - MsgDelivered { chat_id: u32, msg_id: u32 }, + MsgDelivered { + /// ID of the chat which the message belongs to. + chat_id: u32, + + /// ID of the message that was successfully sent. + msg_id: u32, + }, /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_FAILED, see `Message.state`. #[serde(rename_all = "camelCase")] - MsgFailed { chat_id: u32, msg_id: u32 }, + MsgFailed { + /// ID of the chat which the message belongs to. + chat_id: u32, + + /// ID of the message that could not be sent. + msg_id: u32, + }, /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_MDN_RCVD, see `Message.state`. #[serde(rename_all = "camelCase")] - MsgRead { chat_id: u32, msg_id: u32 }, + MsgRead { + /// ID of the chat which the message belongs to. + chat_id: u32, - /// A single message is deleted. + /// ID of the message that was read. + msg_id: u32, + }, + + /// A single message was deleted. + /// + /// This event means that the message will no longer appear in the messagelist. + /// UI should remove the message from the messagelist + /// in response to this event if the message is currently displayed. + /// + /// The message may have been explicitly deleted by the user or expired. + /// Internally the message may have been removed from the database, + /// moved to the trash chat or hidden. + /// + /// This event does not indicate the message + /// deletion from the server. #[serde(rename_all = "camelCase")] - MsgDeleted { chat_id: u32, msg_id: u32 }, + MsgDeleted { + /// ID of the chat where the message was prior to deletion. + /// Never 0. + chat_id: u32, + + /// ID of the deleted message. Never 0. + msg_id: u32, + }, /// Chat changed. The name or the image of a chat group was changed or members were added or removed. /// Or the verify state of a chat has changed. @@ -166,21 +235,35 @@ pub enum EventType { /// Chat ephemeral timer changed. #[serde(rename_all = "camelCase")] - ChatEphemeralTimerModified { chat_id: u32, timer: u32 }, + ChatEphemeralTimerModified { + /// Chat ID. + chat_id: u32, + + /// New ephemeral timer value. + timer: u32, + }, + + /// Chat deleted. + ChatDeleted { + /// Chat ID. + chat_id: u32, + }, /// Contact(s) created, renamed, blocked or deleted. - /// - /// @param data1 (int) If set, this is the contact_id of an added contact that should be selected. #[serde(rename_all = "camelCase")] - ContactsChanged { contact_id: Option }, + ContactsChanged { + /// If set, this is the contact_id of an added contact that should be selected. + contact_id: Option, + }, /// Location of one or more contact has changed. - /// - /// @param data1 (u32) contact_id of the contact for which the location has changed. - /// If the locations of several contacts have been changed, - /// this parameter is set to `None`. #[serde(rename_all = "camelCase")] - LocationChanged { contact_id: Option }, + LocationChanged { + /// contact_id of the contact for which the location has changed. + /// If the locations of several contacts have been changed, + /// this parameter is set to `None`. + contact_id: Option, + }, /// Inform about the configuration progress started by configure(). ConfigureProgress { @@ -195,10 +278,11 @@ pub enum EventType { /// Inform about the import/export progress started by imex(). /// - /// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done - /// @param data2 0 #[serde(rename_all = "camelCase")] - ImexProgress { progress: usize }, + ImexProgress { + /// 0=error, 1-999=progress in permille, 1000=success and done + progress: usize, + }, /// A file has been exported. A file has been written by imex(). /// This event may be sent multiple times by a single call to imex(). @@ -215,26 +299,34 @@ pub enum EventType { /// /// These events are typically sent after a joiner has scanned the QR code /// generated by getChatSecurejoinQrCodeSvg(). - /// - /// @param data1 (int) ID of the contact that wants to join. - /// @param data2 (int) Progress as: - /// 300=vg-/vc-request received, typically shown as "bob@addr joins". - /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - /// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. - /// 1000=Protocol finished for this contact. #[serde(rename_all = "camelCase")] - SecurejoinInviterProgress { contact_id: u32, progress: usize }, + SecurejoinInviterProgress { + /// ID of the contact that wants to join. + contact_id: u32, + + /// Progress as: + /// 300=vg-/vc-request received, typically shown as "bob@addr joins". + /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". + /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. + /// 1000=Protocol finished for this contact. + progress: usize, + }, /// Progress information of a secure-join handshake from the view of the joiner /// (Bob, the person who scans the QR code). /// The events are typically sent while secureJoin(), which /// may take some time, is executed. - /// @param data1 (int) ID of the inviting contact. - /// @param data2 (int) Progress as: - /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." - /// (Bob has verified alice and waits until Alice does the same for him) #[serde(rename_all = "camelCase")] - SecurejoinJoinerProgress { contact_id: u32, progress: usize }, + SecurejoinJoinerProgress { + /// ID of the inviting contact. + contact_id: u32, + + /// Progress as: + /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." + /// (Bob has verified alice and waits until Alice does the same for him) + /// 1000=vg-member-added/vc-contact-confirm received + progress: usize, + }, /// The connectivity to the server changed. /// This means that you should refresh the connectivity view @@ -255,22 +347,37 @@ pub enum EventType { #[serde(rename_all = "camelCase")] WebxdcStatusUpdate { + /// Message ID. msg_id: u32, + + /// Status update ID. status_update_serial: u32, }, /// Data received over an ephemeral peer channel. #[serde(rename_all = "camelCase")] - WebxdcRealtimeData { msg_id: u32, data: Vec }, + WebxdcRealtimeData { + /// Message ID. + msg_id: u32, + + /// Realtime data. + data: Vec, + }, /// Advertisement received over an ephemeral peer channel. /// This can be used by bots to initiate peer-to-peer communication from their side. #[serde(rename_all = "camelCase")] - WebxdcRealtimeAdvertisementReceived { msg_id: u32 }, + WebxdcRealtimeAdvertisementReceived { + /// Message ID of the webxdc instance. + msg_id: u32, + }, /// Inform that a message containing a webxdc instance has been deleted #[serde(rename_all = "camelCase")] - WebxdcInstanceDeleted { msg_id: u32 }, + WebxdcInstanceDeleted { + /// ID of the deleted message. + msg_id: u32, + }, /// Tells that the Background fetch was completed (or timed out). /// This event acts as a marker, when you reach this event you can be sure @@ -286,7 +393,10 @@ pub enum EventType { /// Inform that a single chat list item changed and needs to be rerendered. /// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache. #[serde(rename_all = "camelCase")] - ChatlistItemChanged { chat_id: Option }, + ChatlistItemChanged { + /// ID of the changed chat + chat_id: Option, + }, /// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes) /// @@ -303,7 +413,10 @@ pub enum EventType { AccountsItemChanged, /// Inform than some events have been skipped due to event channel overflow. - EventChannelOverflow { n: u64 }, + EventChannelOverflow { + /// Number of events skipped. + n: u64, + }, } impl From for EventType { @@ -392,6 +505,9 @@ impl From for EventType { timer: timer.to_u32(), } } + CoreEventType::ChatDeleted { chat_id } => ChatDeleted { + chat_id: chat_id.to_u32(), + }, CoreEventType::ContactsChanged(contact) => ContactsChanged { contact_id: contact.map(|c| c.to_u32()), }, diff --git a/deltachat-jsonrpc/src/api/types/login_param.rs b/deltachat-jsonrpc/src/api/types/login_param.rs new file mode 100644 index 0000000000..6036709cdd --- /dev/null +++ b/deltachat-jsonrpc/src/api/types/login_param.rs @@ -0,0 +1,203 @@ +use anyhow::Result; +use deltachat::login_param as dc; +use serde::Deserialize; +use serde::Serialize; +use yerpc::TypeDef; + +/// Login parameters entered by the user. +/// +/// Usually it will be enough to only set `addr` and `password`, +/// and all the other settings will be autoconfigured. +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EnteredLoginParam { + /// Email address. + pub addr: String, + + /// Password. + pub password: String, + + /// Imap server hostname or IP address. + pub imap_server: Option, + + /// Imap server port. + pub imap_port: Option, + + /// Imap socket security. + pub imap_security: Option, + + /// Imap username. + pub imap_user: Option, + + /// SMTP server hostname or IP address. + pub smtp_server: Option, + + /// SMTP server port. + pub smtp_port: Option, + + /// SMTP socket security. + pub smtp_security: Option, + + /// SMTP username. + pub smtp_user: Option, + + /// SMTP Password. + /// + /// Only needs to be specified if different than IMAP password. + pub smtp_password: Option, + + /// TLS options: whether to allow invalid certificates and/or + /// invalid hostnames. + /// Default: Automatic + pub certificate_checks: Option, + + /// If true, login via OAUTH2 (not recommended anymore). + /// Default: false + pub oauth2: Option, +} + +impl From for EnteredLoginParam { + fn from(param: dc::EnteredLoginParam) -> Self { + let imap_security: Socket = param.imap.security.into(); + let smtp_security: Socket = param.smtp.security.into(); + let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into(); + Self { + addr: param.addr, + password: param.imap.password, + imap_server: param.imap.server.into_option(), + imap_port: param.imap.port.into_option(), + imap_security: imap_security.into_option(), + imap_user: param.imap.user.into_option(), + smtp_server: param.smtp.server.into_option(), + smtp_port: param.smtp.port.into_option(), + smtp_security: smtp_security.into_option(), + smtp_user: param.smtp.user.into_option(), + smtp_password: param.smtp.password.into_option(), + certificate_checks: certificate_checks.into_option(), + oauth2: param.oauth2.into_option(), + } + } +} + +impl TryFrom for dc::EnteredLoginParam { + type Error = anyhow::Error; + + fn try_from(param: EnteredLoginParam) -> Result { + Ok(Self { + addr: param.addr, + imap: dc::EnteredServerLoginParam { + server: param.imap_server.unwrap_or_default(), + port: param.imap_port.unwrap_or_default(), + security: param.imap_security.unwrap_or_default().into(), + user: param.imap_user.unwrap_or_default(), + password: param.password, + }, + smtp: dc::EnteredServerLoginParam { + server: param.smtp_server.unwrap_or_default(), + port: param.smtp_port.unwrap_or_default(), + security: param.smtp_security.unwrap_or_default().into(), + user: param.smtp_user.unwrap_or_default(), + password: param.smtp_password.unwrap_or_default(), + }, + certificate_checks: param.certificate_checks.unwrap_or_default().into(), + oauth2: param.oauth2.unwrap_or_default(), + }) + } +} + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Socket { + /// Unspecified socket security, select automatically. + #[default] + Automatic, + + /// TLS connection. + Ssl, + + /// STARTTLS connection. + Starttls, + + /// No TLS, plaintext connection. + Plain, +} + +impl From for Socket { + fn from(value: dc::Socket) -> Self { + match value { + dc::Socket::Automatic => Self::Automatic, + dc::Socket::Ssl => Self::Ssl, + dc::Socket::Starttls => Self::Starttls, + dc::Socket::Plain => Self::Plain, + } + } +} + +impl From for dc::Socket { + fn from(value: Socket) -> Self { + match value { + Socket::Automatic => Self::Automatic, + Socket::Ssl => Self::Ssl, + Socket::Starttls => Self::Starttls, + Socket::Plain => Self::Plain, + } + } +} + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum EnteredCertificateChecks { + /// `Automatic` means that provider database setting should be taken. + /// If there is no provider database setting for certificate checks, + /// check certificates strictly. + #[default] + Automatic, + + /// Ensure that TLS certificate is valid for the server hostname. + Strict, + + /// Accept certificates that are expired, self-signed + /// or otherwise not valid for the server hostname. + AcceptInvalidCertificates, +} + +impl From for EnteredCertificateChecks { + fn from(value: dc::EnteredCertificateChecks) -> Self { + match value { + dc::EnteredCertificateChecks::Automatic => Self::Automatic, + dc::EnteredCertificateChecks::Strict => Self::Strict, + dc::EnteredCertificateChecks::AcceptInvalidCertificates => { + Self::AcceptInvalidCertificates + } + dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => { + Self::AcceptInvalidCertificates + } + } + } +} + +impl From for dc::EnteredCertificateChecks { + fn from(value: EnteredCertificateChecks) -> Self { + match value { + EnteredCertificateChecks::Automatic => Self::Automatic, + EnteredCertificateChecks::Strict => Self::Strict, + EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates, + } + } +} + +trait IntoOption { + fn into_option(self) -> Option; +} +impl IntoOption for T +where + T: Default + std::cmp::PartialEq, +{ + fn into_option(self) -> Option { + if self == T::default() { + None + } else { + Some(self) + } + } +} diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index c2ca8fe7b4..9cb8fc04c8 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::api::VcardContact; use anyhow::{Context as _, Result}; use deltachat::chat::Chat; @@ -6,6 +8,7 @@ use deltachat::chat::ChatVisibility; use deltachat::contact::Contact; use deltachat::context::Context; use deltachat::download; +use deltachat::log::LogExt as _; use deltachat::message::Message; use deltachat::message::MsgId; use deltachat::message::Viewtype; @@ -37,6 +40,8 @@ pub struct MessageObject { text: String, + is_edited: bool, + /// Check if a message has a POI location bound to it. /// These locations are also returned by `get_locations` method. /// The UI may decide to display a special icon beside such messages. @@ -66,6 +71,9 @@ pub struct MessageObject { /// when is_info is true this describes what type of system message it is system_message_type: SystemMessageType, + /// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped. + info_contact_id: Option, + duration: i32, dimensions_height: i32, dimensions_width: i32, @@ -89,6 +97,10 @@ pub struct MessageObject { download_state: DownloadState, + original_msg_id: Option, + + saved_message_id: Option, + reactions: Option, vcard_contact: Option, @@ -104,6 +116,9 @@ enum MessageQuote { WithMessage { text: String, message_id: u32, + /// The quoted message does not always belong + /// to the same chat, e.g. when "Reply Privately" is used. + chat_id: u32, author_display_name: String, author_display_color: String, override_sender_name: Option, @@ -129,7 +144,10 @@ impl MessageObject { let override_sender_name = message.get_override_sender_name(); let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc { - Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?) + WebxdcMessageInfo::get_for_message(context, msg_id) + .await + .log_err(context) + .ok() } else { None }; @@ -147,6 +165,7 @@ impl MessageObject { Some(MessageQuote::WithMessage { text: quoted_text, message_id: quote.get_id().to_u32(), + chat_id: quote.get_chat_id().to_u32(), author_display_name: quote_author.get_display_name().to_owned(), author_display_color: color_int_to_hex_string(quote_author.get_color()), override_sender_name: quote.get_override_sender_name(), @@ -194,6 +213,7 @@ impl MessageObject { quote, parent_id, text: message.get_text(), + is_edited: message.is_edited(), has_location: message.has_location(), has_html: message.has_html(), view_type: message.get_viewtype().into(), @@ -215,6 +235,10 @@ impl MessageObject { is_forwarded: message.is_forwarded(), is_bot: message.is_bot(), system_message_type: message.get_info_type().into(), + info_contact_id: message + .get_info_contact_id(context) + .await? + .map(|id| id.to_u32()), duration: message.get_duration(), dimensions_height: message.get_height(), @@ -249,6 +273,16 @@ impl MessageObject { download_state, + original_msg_id: message + .get_original_msg_id(context) + .await? + .map(|id| id.to_u32()), + + saved_message_id: message + .get_saved_msg_id(context) + .await? + .map(|id| id.to_u32()), + reactions, vcard_contact: vcard_contacts.first().cloned(), @@ -589,6 +623,7 @@ pub struct MessageData { pub html: Option, pub viewtype: Option, pub file: Option, + pub filename: Option, pub location: Option<(f64, f64)>, pub override_sender_name: Option, /// Quoted message id. Takes preference over `quoted_text` (see below). @@ -613,7 +648,12 @@ impl MessageData { message.set_override_sender_name(self.override_sender_name); } if let Some(file) = self.file { - message.set_file(file, None); + message.set_file_and_deduplicate( + context, + Path::new(&file), + self.filename.as_deref(), + None, + )?; } if let Some((latitude, longitude)) = self.location { message.set_location(latitude, longitude); @@ -644,7 +684,6 @@ pub struct MessageReadReceipt { #[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MessageInfo { - rawtext: String, ephemeral_timer: EphemeralTimer, /// When message is ephemeral this contains the timestamp of the message expiry ephemeral_timestamp: Option, @@ -657,7 +696,6 @@ pub struct MessageInfo { impl MessageInfo { pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result { let message = Message::load_from_db(context, msg_id).await?; - let rawtext = msg_id.rawtext(context).await?; let ephemeral_timer = message.get_ephemeral_timer().into(); let ephemeral_timestamp = match message.get_ephemeral_timer() { deltachat::ephemeral::Timer::Disabled => None, @@ -670,7 +708,6 @@ impl MessageInfo { let hop_info = msg_id.hop_info(context).await?; Ok(Self { - rawtext, ephemeral_timer, ephemeral_timestamp, error: message.error(), diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs index 8143be73db..995e931bff 100644 --- a/deltachat-jsonrpc/src/api/types/mod.rs +++ b/deltachat-jsonrpc/src/api/types/mod.rs @@ -5,6 +5,7 @@ pub mod contact; pub mod events; pub mod http; pub mod location; +pub mod login_param; pub mod message; pub mod provider_info; pub mod qr; diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index c2588ee484..61d8141f76 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -63,6 +63,7 @@ pub enum QrObject { /// Iroh node address. node_addr: String, }, + BackupTooNew {}, /// Ask the user if they want to use the given service for video chats. WebrtcInstance { domain: String, @@ -100,11 +101,15 @@ pub enum QrObject { /// URL scanned. /// /// Ask the user if they want to open a browser or copy the URL to clipboard. - Url { url: String }, + Url { + url: String, + }, /// Text scanned. /// /// Ask the user if they want to copy the text to clipboard. - Text { text: String }, + Text { + text: String, + }, /// Ask the user if they want to withdraw their own QR code. WithdrawVerifyContact { /// Contact ID. @@ -160,7 +165,9 @@ pub enum QrObject { /// `dclogin:` scheme parameters. /// /// Ask the user if they want to login with the email address. - Login { address: String }, + Login { + address: String, + }, } impl From for QrObject { @@ -217,6 +224,7 @@ impl From for QrObject { node_addr: serde_json::to_string(node_addr).unwrap_or_default(), auth_token, }, + Qr::BackupTooNew {} => QrObject::BackupTooNew {}, Qr::WebrtcInstance { domain, instance_pattern, diff --git a/deltachat-jsonrpc/src/webserver.rs b/deltachat-jsonrpc/src/webserver.rs deleted file mode 100644 index 512372c0f9..0000000000 --- a/deltachat-jsonrpc/src/webserver.rs +++ /dev/null @@ -1,47 +0,0 @@ -#![recursion_limit = "256"] -use std::net::SocketAddr; -use std::path::PathBuf; - -use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router}; -use yerpc::axum::handle_ws_rpc; -use yerpc::{RpcClient, RpcSession}; - -mod api; -use api::{Accounts, CommandApi}; - -const DEFAULT_PORT: u16 = 20808; - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<(), std::io::Error> { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - - let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string()); - let port = std::env::var("DC_PORT") - .map(|port| port.parse::().expect("DC_PORT must be a number")) - .unwrap_or(DEFAULT_PORT); - log::info!("Starting with accounts directory `{path}`."); - let writable = true; - let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap(); - let state = CommandApi::new(accounts); - - let app = Router::new() - .route("/ws", get(handler)) - .layer(Extension(state.clone())); - - tokio::spawn(async move { - state.accounts.write().await.start_io().await; - }); - - let addr = SocketAddr::from(([127, 0, 0, 1], port)); - log::info!("JSON-RPC WebSocket server listening on {}", addr); - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); - - Ok(()) -} - -async fn handler(ws: WebSocketUpgrade, Extension(api): Extension) -> Response { - let (client, out_receiver) = RpcClient::new(); - let session = RpcSession::new(client.clone(), api.clone()); - handle_ws_rpc(ws, out_receiver, session).await -} diff --git a/deltachat-jsonrpc/typescript/example.html b/deltachat-jsonrpc/typescript/example.html deleted file mode 100644 index 1f5ca16717..0000000000 --- a/deltachat-jsonrpc/typescript/example.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - DeltaChat JSON-RPC example - - - - -

DeltaChat JSON-RPC example

-
- -
-

log

-
-

- Tip: open the dev console and use the client with - window.client -

- - diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts deleted file mode 100644 index e45bc18ccd..0000000000 --- a/deltachat-jsonrpc/typescript/example/example.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { DcEvent, DeltaChat } from "../deltachat.js"; - -var SELECTED_ACCOUNT = 0; - -window.addEventListener("DOMContentLoaded", (_event) => { - (window as any).selectDeltaAccount = (id: string) => { - SELECTED_ACCOUNT = Number(id); - window.dispatchEvent(new Event("account-changed")); - }; - console.log("launch run script..."); - run().catch((err) => console.error("run failed", err)); -}); - -async function run() { - const $main = document.getElementById("main")!; - const $side = document.getElementById("side")!; - const $head = document.getElementById("header")!; - - const client = new DeltaChat("ws://localhost:20808/ws"); - - (window as any).client = client.rpc; - - client.on("ALL", (accountId, event) => { - onIncomingEvent(accountId, event); - }); - - window.addEventListener("account-changed", async (_event: Event) => { - listChatsForSelectedAccount(); - }); - - await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]); - - async function loadAccountsInHeader() { - console.log("load accounts"); - const accounts = await client.rpc.getAllAccounts(); - console.log("accounts loaded", accounts); - for (const account of accounts) { - if (account.kind === "Configured") { - write( - $head, - ` - ${account.id}: ${account.addr!} -  ` - ); - } else { - write( - $head, - ` - ${account.id}: (unconfigured) -  ` - ); - } - } - } - - async function listChatsForSelectedAccount() { - clear($main); - const selectedAccount = SELECTED_ACCOUNT; - const info = await client.rpc.getAccountInfo(selectedAccount); - if (info.kind !== "Configured") { - return write($main, "Account is not configured"); - } - write($main, `

${info.addr!}

`); - const chats = await client.rpc.getChatlistEntries( - selectedAccount, - 0, - null, - null - ); - for (const chatId of chats) { - const chat = await client.rpc.getFullChatById(selectedAccount, chatId); - write($main, `

${chat.name}

`); - const messageIds = await client.rpc.getMessageIds( - selectedAccount, - chatId, - false, - false - ); - const messages = await client.rpc.getMessages( - selectedAccount, - messageIds - ); - for (const [_messageId, message] of Object.entries(messages)) { - if (message.kind === "message") write($main, `

${message.text}

`); - else write($main, `

loading error: ${message.error}

`); - } - } - } - - function onIncomingEvent(accountId: number, event: DcEvent) { - write( - $side, - ` -

- [${event.kind} on account ${accountId}]
- f1: ${JSON.stringify( - Object.assign({}, event, { kind: undefined }) - )} -

` - ); - } -} - -function write(el: HTMLElement, html: string) { - el.innerHTML += html; -} -function clear(el: HTMLElement) { - el.innerHTML = ""; -} diff --git a/deltachat-jsonrpc/typescript/example/node-add-account.js b/deltachat-jsonrpc/typescript/example/node-add-account.js deleted file mode 100644 index df47c39c92..0000000000 --- a/deltachat-jsonrpc/typescript/example/node-add-account.js +++ /dev/null @@ -1,29 +0,0 @@ -import { DeltaChat } from "../dist/deltachat.js"; - -run().catch(console.error); - -async function run() { - const delta = new DeltaChat("ws://localhost:20808/ws"); - delta.on("event", (event) => { - console.log("event", event.data); - }); - - const email = process.argv[2]; - const password = process.argv[3]; - if (!email || !password) - throw new Error( - "USAGE: node node-add-account.js " - ); - console.log(`creating account for ${email}`); - const id = await delta.rpc.addAccount(); - console.log(`created account id ${id}`); - await delta.rpc.setConfig(id, "addr", email); - await delta.rpc.setConfig(id, "mail_pw", password); - console.log("configuration updated"); - await delta.rpc.configure(id); - console.log("account configured!"); - - const accounts = await delta.rpc.getAllAccounts(); - console.log("accounts", accounts); - console.log("waiting for events..."); -} diff --git a/deltachat-jsonrpc/typescript/example/node-demo.js b/deltachat-jsonrpc/typescript/example/node-demo.js deleted file mode 100644 index 83ea89b3fd..0000000000 --- a/deltachat-jsonrpc/typescript/example/node-demo.js +++ /dev/null @@ -1,14 +0,0 @@ -import { DeltaChat } from "../dist/deltachat.js"; - -run().catch(console.error); - -async function run() { - const delta = new DeltaChat(); - delta.on("event", (event) => { - console.log("event", event.data); - }); - - const accounts = await delta.rpc.getAllAccounts(); - console.log("accounts", accounts); - console.log("waiting for events..."); -} diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index b7a7d34475..85a2866760 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -34,7 +34,7 @@ "name": "@deltachat/jsonrpc-client", "repository": { "type": "git", - "url": "https://github.com/deltachat/deltachat-core-rust.git" + "url": "https://github.com/chatmail/core.git" }, "scripts": { "build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs", @@ -42,10 +42,6 @@ "build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs", "build:tsc": "tsc", "docs": "typedoc --out docs deltachat.ts", - "example": "run-s build example:build example:start", - "example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js", - "example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.", - "example:start": "http-server .", "extract-constants": "node ./scripts/generate-constants.js", "generate-bindings": "cargo test", "prettier:check": "prettier --check .", @@ -58,5 +54,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.155.2" + "version": "1.159.5" } diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts index 83cc2f7e71..32b86e7386 100644 --- a/deltachat-jsonrpc/typescript/src/client.ts +++ b/deltachat-jsonrpc/typescript/src/client.ts @@ -2,7 +2,7 @@ import * as T from "../generated/types.js"; import { EventType } from "../generated/types.js"; import * as RPC from "../generated/jsonrpc.js"; import { RawClient } from "../generated/client.js"; -import { WebsocketTransport, BaseTransport, Request } from "yerpc"; +import { BaseTransport, Request } from "yerpc"; import { TinyEmitter } from "@deltachat/tiny-emitter"; type Events = { ALL: (accountId: number, event: EventType) => void } & { @@ -74,34 +74,6 @@ export class BaseDeltaChat< } } -export type Opts = { - url: string; - startEventLoop: boolean; -}; - -export const DEFAULT_OPTS: Opts = { - url: "ws://localhost:20808/ws", - startEventLoop: true, -}; -export class DeltaChat extends BaseDeltaChat { - opts: Opts; - close() { - this.transport.close(); - } - constructor(opts?: Opts | string) { - if (typeof opts === "string") { - opts = { ...DEFAULT_OPTS, url: opts }; - } else if (opts) { - opts = { ...DEFAULT_OPTS, ...opts }; - } else { - opts = { ...DEFAULT_OPTS }; - } - const transport = new WebsocketTransport(opts.url); - super(transport, opts.startEventLoop); - this.opts = opts; - } -} - export class StdioDeltaChat extends BaseDeltaChat { close() {} constructor(input: any, output: any, startEventLoop: boolean) { diff --git a/deltachat-jsonrpc/typescript/test/basic.ts b/deltachat-jsonrpc/typescript/test/basic.ts index d5ce57aecb..b3ba3b6fa0 100644 --- a/deltachat-jsonrpc/typescript/test/basic.ts +++ b/deltachat-jsonrpc/typescript/test/basic.ts @@ -1,4 +1,3 @@ -import { strictEqual } from "assert"; import chai, { assert, expect } from "chai"; import chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 2d90e80606..e92071426b 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -1,5 +1,5 @@ import { assert, expect } from "chai"; -import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js"; +import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js"; import { RpcServerHandle, createTempUser, startServer } from "./test_base.js"; const EVENT_TIMEOUT = 20000; @@ -80,11 +80,8 @@ describe("online tests", function () { } this.timeout(15000); - const contactId = await dc.rpc.createContact( - accountId1, - account2.email, - null - ); + const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]); + const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0]; const chatId = await dc.rpc.createChatByContactId(accountId1, contactId); const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2); @@ -101,20 +98,18 @@ describe("online tests", function () { expect(messageList).have.length(1); const message = await dc.rpc.getMessage(accountId2, messageList[0]); expect(message.text).equal("Hello"); + expect(message.showPadlock).equal(true); }); - it("send and receive text message roundtrip, encrypted on answer onwards", async function () { + it("send and receive text message roundtrip", async function () { if (!accountsConfigured) { this.skip(); } this.timeout(10000); // send message from A to B - const contactId = await dc.rpc.createContact( - accountId1, - account2.email, - null - ); + const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]); + const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0]; const chatId = await dc.rpc.createChatByContactId(accountId1, contactId); const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2); dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2"); diff --git a/deltachat-jsonrpc/typescript/tsconfig.json b/deltachat-jsonrpc/typescript/tsconfig.json index bbb699cf45..70cf3ca0e0 100644 --- a/deltachat-jsonrpc/typescript/tsconfig.json +++ b/deltachat-jsonrpc/typescript/tsconfig.json @@ -15,6 +15,6 @@ "noImplicitAny": true, "isolatedModules": true }, - "include": ["*.ts", "example/*.ts", "test/*.ts"], + "include": ["*.ts", "test/*.ts"], "compileOnSave": false } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index e4b3f1ef2a..f89ad6e1e5 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "deltachat-repl" -version = "1.155.2" +version = "1.159.5" license = "MPL-2.0" edition = "2021" -repository = "https://github.com/deltachat/deltachat-core-rust" +repository = "https://github.com/chatmail/core" [dependencies] anyhow = { workspace = true } deltachat = { workspace = true, features = ["internals"]} -dirs = "5" +dirs = "6" log = { workspace = true } nu-ansi-term = { workspace = true } qr2term = "0.3.3" diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index ae676143de..0aceb14b8d 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -92,7 +92,7 @@ async fn reset_tables(context: &Context, bits: i32) { context.emit_msgs_changed_without_ids(); } -async fn poke_eml_file(context: &Context, filename: impl AsRef) -> Result<()> { +async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> { let data = read_file(context, filename).await?; if let Err(err) = receive_imf(context, &data, false).await { @@ -126,7 +126,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { real_spec = rs.unwrap(); } if let Some(suffix) = get_filesuffix_lc(&real_spec) { - if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() { + if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() { read_cnt += 1 } } else { @@ -140,7 +140,10 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { if name.ends_with(".eml") { let path_plus_name = format!("{}/{}", &real_spec, name); println!("Import: {path_plus_name}"); - if poke_eml_file(context, path_plus_name).await.is_ok() { + if poke_eml_file(context, Path::new(&path_plus_name)) + .await + .is_ok() + { read_cnt += 1 } } @@ -1159,17 +1162,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let reaction = arg2; send_reaction(&context, msg_id, reaction).await?; } - "listcontacts" | "contacts" | "listverified" => { - let contacts = Contact::get_all( - &context, - if arg0 == "listverified" { - DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF - } else { - DC_GCL_ADD_SELF - }, - Some(arg1), - ) - .await?; + "listcontacts" | "contacts" => { + let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?; log_contactlist(&context, &contacts).await?; println!("{} contacts.", contacts.len()); } @@ -1278,7 +1272,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "fileinfo" => { ensure!(!arg1.is_empty(), "Argument missing."); - if let Ok(buf) = read_file(&context, &arg1).await { + if let Ok(buf) = read_file(&context, Path::new(arg1)).await { let (width, height) = get_filemeta(&buf)?; println!("width={width}, height={height}"); } else { diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs index 52a7a87549..8d3dfc3eb1 100644 --- a/deltachat-repl/src/main.rs +++ b/deltachat-repl/src/main.rs @@ -323,7 +323,7 @@ async fn start(args: Vec) -> Result<(), Error> { } }); - println!("Delta Chat Core is awaiting your commands."); + println!("Chatmail is awaiting your commands."); let config = Config::builder() .history_ignore_space(true) diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index acb7a5430a..4fd75e90d3 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat-rpc-client" -version = "1.155.2" +version = "1.159.5" description = "Python client for Delta Chat core JSON-RPC interface" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -70,3 +70,11 @@ line-length = 120 [tool.isort] profile = "black" + +[dependency-groups] +dev = [ + "imap-tools", + "pytest", + "pytest-timeout", + "pytest-xdist", +] diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 2fef315ef2..1a532c7011 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,4 +1,4 @@ -"""Delta Chat JSON-RPC high-level API""" +"""Delta Chat JSON-RPC high-level API.""" from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py index 8849e72ea0..47d9b878cd 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py @@ -1,4 +1,5 @@ import argparse +import os import re import sys from threading import Thread @@ -89,8 +90,8 @@ def _run_cli( help="accounts folder (default: current working directory)", nargs="?", ) - parser.add_argument("--email", action="store", help="email address") - parser.add_argument("--password", action="store", help="password") + parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL")) + parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD")) args = parser.parse_args(argv[1:]) with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: @@ -114,7 +115,7 @@ def _run_cli( def extract_addr(text: str) -> str: - """extract email address from the given text.""" + """Extract email address from the given text.""" match = re.match(r".*\((.+@.+)\)", text) if match: text = match.group(1) @@ -123,7 +124,7 @@ def extract_addr(text: str) -> str: def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]: - """return image changed/deleted info from parsing the given system message text.""" + """Return image changed/deleted info from parsing the given system message text.""" text = text.lower() match = re.match(r"group image (changed|deleted) by (.+).", text) if match: @@ -142,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]: def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]: - """return add/remove info from parsing the given system message text. + """Return add/remove info from parsing the given system message text. returns a (action, affected, actor) tuple. """ diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 1648ebad6b..11d76ff049 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -1,3 +1,5 @@ +"""Account module.""" + from __future__ import annotations from dataclasses import dataclass @@ -26,18 +28,36 @@ class Account: def _rpc(self) -> "Rpc": return self.manager.rpc - def wait_for_event(self) -> AttrDict: + def wait_for_event(self, event_type=None) -> AttrDict: """Wait until the next event and return it.""" - return AttrDict(self._rpc.wait_for_event(self.id)) + while True: + next_event = AttrDict(self._rpc.wait_for_event(self.id)) + if event_type is None or next_event.kind == event_type: + return next_event def clear_all_events(self): - """Removes all queued-up events for a given account. Useful for tests.""" + """Remove all queued-up events for a given account. + + Useful for tests. + """ self._rpc.clear_all_events(self.id) def remove(self) -> None: """Remove the account.""" self._rpc.remove_account(self.id) + def clone(self) -> "Account": + """Clone given account. + + This uses backup-transfer via iroh, i.e. the 'Add second device' feature. + """ + future = self._rpc.provide_backup.future(self.id) + qr = self._rpc.get_backup_qr(self.id) + new_account = self.manager.add_account() + new_account._rpc.get_backup(new_account.id, qr) + future() + return new_account + def start_io(self) -> None: """Start the account I/O.""" self._rpc.start_io(self.id) @@ -67,7 +87,7 @@ def get_config(self, key: str) -> Optional[str]: return self._rpc.get_config(self.id, key) def update_config(self, **kwargs) -> None: - """update config values.""" + """Update config values.""" for key, value in kwargs.items(): self.set_config(key, value) @@ -83,9 +103,15 @@ def get_avatar(self) -> Optional[str]: return self.get_config("selfavatar") def check_qr(self, qr): + """Parse QR code contents. + + This function takes the raw text scanned + and checks what can be done with it. + """ return self._rpc.check_qr(self.id, qr) def set_config_from_qr(self, qr: str): + """Set configuration values from a QR code.""" self._rpc.set_config_from_qr(self.id, qr) @futuremethod @@ -93,15 +119,23 @@ def configure(self): """Configure an account.""" yield self._rpc.configure.future(self.id) + @futuremethod + def add_or_update_transport(self, params): + """Add a new transport.""" + yield self._rpc.add_or_update_transport.future(self.id, params) + + @futuremethod + def list_transports(self): + """Return the list of all email accounts that are used as a transport in the current profile.""" + transports = yield self._rpc.list_transports.future(self.id) + return transports + def bring_online(self): """Start I/O and wait until IMAP becomes IDLE.""" self.start_io() - while True: - event = self.wait_for_event() - if event.kind == EventType.IMAP_INBOX_IDLE: - break + self.wait_for_event(EventType.IMAP_INBOX_IDLE) - def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact: + def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact: """Create a new Contact or return an existing one. Calling this method will always result in the same @@ -109,19 +143,42 @@ def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = No with that e-mail address, it is unblocked and its display name is updated if specified. - :param obj: email-address or contact id. + :param obj: email-address, contact id or account. :param name: (optional) display name for this contact. """ + if isinstance(obj, Account): + vcard = obj.self_contact.make_vcard() + [contact] = self.import_vcard(vcard) + if name: + contact.set_name(name) + return contact if isinstance(obj, int): obj = Contact(self, obj) if isinstance(obj, Contact): obj = obj.get_snapshot().address return Contact(self, self._rpc.create_contact(self.id, obj, name)) + def make_vcard(self, contacts: list[Contact]) -> str: + """Create vCard with the given contacts.""" + assert all(contact.account == self for contact in contacts) + contact_ids = [contact.id for contact in contacts] + return self._rpc.make_vcard(self.id, contact_ids) + + def import_vcard(self, vcard: str) -> list[Contact]: + """Import vCard. + + Return created or modified contacts in the order they appear in vCard. + """ + contact_ids = self._rpc.import_vcard_contents(self.id, vcard) + return [Contact(self, contact_id) for contact_id in contact_ids] + def create_chat(self, account: "Account") -> Chat: - addr = account.get_config("addr") - contact = self.create_contact(addr) - return contact.create_chat() + """Create a 1:1 chat with another account.""" + return self.create_contact(account).create_chat() + + def get_device_chat(self) -> Chat: + """Return device chat.""" + return self.device_contact.create_chat() def get_contact_by_id(self, contact_id: int) -> Contact: """Return Contact instance for the given contact ID.""" @@ -154,8 +211,8 @@ def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]: def get_contacts( self, query: Optional[str] = None, + *, with_self: bool = False, - verified_only: bool = False, snapshot: bool = False, ) -> Union[list[Contact], list[AttrDict]]: """Get a filtered list of contacts. @@ -163,12 +220,9 @@ def get_contacts( :param query: if a string is specified, only return contacts whose name or e-mail matches query. :param with_self: if True the self-contact is also included if it matches the query. - :param only_verified: if True only return verified contacts. :param snapshot: If True return a list of contact snapshots instead of Contact instances. """ flags = 0 - if verified_only: - flags |= ContactFlag.VERIFIED_ONLY if with_self: flags |= ContactFlag.ADD_SELF @@ -180,9 +234,14 @@ def get_contacts( @property def self_contact(self) -> Contact: - """This account's identity as a Contact.""" + """Account's identity as a Contact.""" return Contact(self, SpecialContactId.SELF) + @property + def device_contact(self) -> Chat: + """Account's device contact.""" + return Contact(self, SpecialContactId.DEVICE) + def get_chatlist( self, query: Optional[str] = None, @@ -238,8 +297,7 @@ def get_chat_by_id(self, chat_id: int) -> Chat: return Chat(self, chat_id) def secure_join(self, qrdata: str) -> Chat: - """Continue a Setup-Contact or Verified-Group-Invite protocol started on - another device. + """Continue a Setup-Contact or Verified-Group-Invite protocol started on another device. The function returns immediately and the handshake runs in background, sending and receiving several messages. @@ -296,34 +354,40 @@ def wait_next_messages(self) -> list[Message]: def wait_for_incoming_msg_event(self): """Wait for incoming message event and return it.""" - while True: - event = self.wait_for_event() - if event.kind == EventType.INCOMING_MSG: - return event + return self.wait_for_event(EventType.INCOMING_MSG) + + def wait_for_msgs_changed_event(self): + """Wait for messages changed event and return it.""" + return self.wait_for_event(EventType.MSGS_CHANGED) + + def wait_for_msgs_noticed_event(self): + """Wait for messages noticed event and return it.""" + return self.wait_for_event(EventType.MSGS_NOTICED) def wait_for_incoming_msg(self): """Wait for incoming message and return it. - Consumes all events before the next incoming message event.""" + Consumes all events before the next incoming message event. + """ return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id) def wait_for_securejoin_inviter_success(self): + """Wait until SecureJoin process finishes successfully on the inviter side.""" while True: event = self.wait_for_event() if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000: break def wait_for_securejoin_joiner_success(self): + """Wait until SecureJoin process finishes successfully on the joiner side.""" while True: event = self.wait_for_event() if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000: break def wait_for_reactions_changed(self): - while True: - event = self.wait_for_event() - if event.kind == EventType.REACTIONS_CHANGED: - return event + """Wait for reaction change event.""" + return self.wait_for_event(EventType.REACTIONS_CHANGED) def get_fresh_messages_in_arrival_order(self) -> list[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" @@ -352,3 +416,7 @@ def import_self_keys(self, path) -> None: """Import keys.""" passphrase = "" # Importing passphrase-protected keys is currently not supported. self._rpc.import_self_keys(self.id, str(path), passphrase) + + def initiate_autocrypt_key_transfer(self) -> None: + """Send Autocrypt Setup Message.""" + return self._rpc.initiate_autocrypt_key_transfer(self.id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 9d33a5a0f4..fa4006e0fd 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -1,3 +1,5 @@ +"""Chat module.""" + from __future__ import annotations import calendar @@ -89,7 +91,8 @@ def set_name(self, name: str) -> None: 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.""" + 0 means the timer is disabled, use 1 for immediate deletion. + """ self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer) def get_encryption_info(self) -> str: @@ -124,6 +127,7 @@ def send_message( html: Optional[str] = None, viewtype: Optional[ViewType] = None, file: Optional[str] = None, + filename: Optional[str] = None, location: Optional[tuple[float, float]] = None, override_sender_name: Optional[str] = None, quoted_msg: Optional[Union[int, Message]] = None, @@ -137,6 +141,7 @@ def send_message( "html": html, "viewtype": viewtype, "file": file, + "filename": filename, "location": location, "overrideSenderName": override_sender_name, "quotedMessageId": quoted_msg, @@ -172,13 +177,14 @@ def set_draft( self, text: Optional[str] = None, file: Optional[str] = None, + filename: Optional[str] = None, quoted_msg: Optional[int] = None, viewtype: Optional[str] = None, ) -> None: """Set draft message.""" if isinstance(quoted_msg, Message): quoted_msg = quoted_msg.id - self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype) + self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype) def remove_draft(self) -> None: """Remove draft message.""" @@ -196,12 +202,12 @@ def get_draft(self) -> Optional[AttrDict]: return snapshot def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]: - """get the list of messages in this chat.""" + """Get the list of messages in this chat.""" msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) return [Message(self.account, msg_id) for msg_id in msgs] def get_fresh_message_count(self) -> int: - """Get number of fresh messages in this chat""" + """Get number of fresh messages in this chat.""" return self._rpc.get_fresh_msg_cnt(self.account.id, self.id) def mark_noticed(self) -> None: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index cceda316eb..a62c22deb3 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -48,6 +48,7 @@ def __init__( self.add_hooks(hooks or []) def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None: + """Register multiple hooks.""" for hook, event in hooks: self.add_hook(hook, event) @@ -77,9 +78,11 @@ def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None: self._hooks.get(type(event), set()).remove((hook, event)) def is_configured(self) -> bool: + """Return True if the client is configured.""" return self.account.is_configured() def configure(self, email: str, password: str, **kwargs) -> None: + """Configure the client.""" self.account.set_config("addr", email) self.account.set_config("mail_pw", password) for key, value in kwargs.items(): @@ -198,5 +201,6 @@ class Bot(Client): """Simple bot implementation that listens to events of a single account.""" def configure(self, email: str, password: str, **kwargs) -> None: + """Configure the bot.""" kwargs.setdefault("bot", "1") super().configure(email, password, **kwargs) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py index 5268a281c6..6ba25bdeb7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py @@ -1,14 +1,19 @@ +"""Constants module.""" + from enum import Enum, IntEnum COMMAND_PREFIX = "/" class ContactFlag(IntEnum): - VERIFIED_ONLY = 0x01 + """Bit flags for get_contacts() method.""" + ADD_SELF = 0x02 class ChatlistFlag(IntEnum): + """Bit flags for get_chatlist() method.""" + ARCHIVED_ONLY = 0x01 NO_SPECIALS = 0x02 ADD_ALLDONE_HINT = 0x04 @@ -16,6 +21,8 @@ class ChatlistFlag(IntEnum): class SpecialContactId(IntEnum): + """Special contact IDs.""" + SELF = 1 INFO = 2 # centered messages as "member added", used in all chats DEVICE = 5 # messages "update info" in the device-chat @@ -23,7 +30,7 @@ class SpecialContactId(IntEnum): class EventType(str, Enum): - """Core event types""" + """Core event types.""" INFO = "Info" SMTP_CONNECTED = "SmtpConnected" @@ -48,6 +55,7 @@ class EventType(str, Enum): MSG_READ = "MsgRead" MSG_DELETED = "MsgDeleted" CHAT_MODIFIED = "ChatModified" + CHAT_DELETED = "ChatDeleted" CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified" CONTACTS_CHANGED = "ContactsChanged" LOCATION_CHANGED = "LocationChanged" @@ -70,7 +78,7 @@ class EventType(str, Enum): class ChatId(IntEnum): - """Special chat ids""" + """Special chat IDs.""" TRASH = 3 ARCHIVED_LINK = 6 @@ -79,7 +87,7 @@ class ChatId(IntEnum): class ChatType(IntEnum): - """Chat types""" + """Chat type.""" UNDEFINED = 0 SINGLE = 100 @@ -89,7 +97,7 @@ class ChatType(IntEnum): class ChatVisibility(str, Enum): - """Chat visibility types""" + """Chat visibility types.""" NORMAL = "Normal" ARCHIVED = "Archived" @@ -97,7 +105,7 @@ class ChatVisibility(str, Enum): class DownloadState(str, Enum): - """Message download state""" + """Message download state.""" DONE = "Done" AVAILABLE = "Available" @@ -158,14 +166,14 @@ class MessageState(IntEnum): class MessageId(IntEnum): - """Special message ids""" + """Special message IDs.""" DAYMARKER = 9 LAST_SPECIAL = 9 class CertificateChecks(IntEnum): - """Certificate checks mode""" + """Certificate checks mode.""" AUTOMATIC = 0 STRICT = 1 @@ -173,7 +181,7 @@ class CertificateChecks(IntEnum): class Connectivity(IntEnum): - """Connectivity states""" + """Connectivity states.""" NOT_CONNECTED = 1000 CONNECTING = 2000 @@ -182,7 +190,7 @@ class Connectivity(IntEnum): class KeyGenType(IntEnum): - """Type of the key to generate""" + """Type of the key to generate.""" DEFAULT = 0 RSA2048 = 1 @@ -192,21 +200,21 @@ class KeyGenType(IntEnum): # "Lp" means "login parameters" class LpAuthFlag(IntEnum): - """Authorization flags""" + """Authorization flags.""" OAUTH2 = 0x2 NORMAL = 0x4 class MediaQuality(IntEnum): - """Media quality setting""" + """Media quality setting.""" BALANCED = 0 WORSE = 1 class ProviderStatus(IntEnum): - """Provider status according to manual testing""" + """Provider status according to manual testing.""" OK = 1 PREPARATION = 2 @@ -214,7 +222,7 @@ class ProviderStatus(IntEnum): class PushNotifyState(IntEnum): - """Push notifications state""" + """Push notifications state.""" NOT_CONNECTED = 0 HEARTBEAT = 1 @@ -222,7 +230,7 @@ class PushNotifyState(IntEnum): class ShowEmails(IntEnum): - """Show emails mode""" + """Show emails mode.""" OFF = 0 ACCEPTED_CONTACTS = 1 @@ -230,7 +238,7 @@ class ShowEmails(IntEnum): class SocketSecurity(IntEnum): - """Socket security""" + """Socket security.""" AUTOMATIC = 0 SSL = 1 @@ -239,7 +247,7 @@ class SocketSecurity(IntEnum): class VideochatType(IntEnum): - """Video chat URL type""" + """Video chat URL type.""" UNKNOWN = 0 BASICWEBRTC = 1 diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index 81c4bba59d..c2444afbd2 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -1,3 +1,5 @@ +"""Contact module.""" + from dataclasses import dataclass from typing import TYPE_CHECKING @@ -11,8 +13,7 @@ @dataclass class Contact: - """ - Contact API. + """Contact API. Essentially a wrapper for RPC, account ID and a contact ID. """ @@ -45,8 +46,9 @@ def set_name(self, name: str) -> None: self._rpc.change_contact_name(self.account.id, self.id, name) def get_encryption_info(self) -> str: - """Get a multi-line encryption info, containing your fingerprint and - the fingerprint of the contact. + """Get a multi-line encryption info. + + Encryption info contains your fingerprint and the fingerprint of the contact. """ return self._rpc.get_contact_encryption_info(self.account.id, self.id) @@ -66,4 +68,5 @@ def create_chat(self) -> "Chat": ) def make_vcard(self) -> str: - return self._rpc.make_vcard(self.account.id, [self.id]) + """Make a vCard for the contact.""" + return self.account.make_vcard([self]) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index c972a865e2..58ee30b322 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -1,3 +1,5 @@ +"""Account manager module.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -10,12 +12,13 @@ class DeltaChat: - """ - Delta Chat accounts manager. + """Delta Chat accounts manager. + This is the root of the object oriented API. """ def __init__(self, rpc: "Rpc") -> None: + """Initialize account manager.""" self.rpc = rpc def add_account(self) -> Account: @@ -37,9 +40,7 @@ def stop_io(self) -> None: self.rpc.stop_io_for_all_accounts() def maybe_network(self) -> None: - """Indicate that the network likely has come back or just that the network - conditions might have changed. - """ + """Indicate that the network conditions might have changed.""" self.rpc.maybe_network() def get_system_info(self) -> AttrDict: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index ca46f5b77d..98dfbc6801 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -36,7 +36,7 @@ def __init__(self, func: Optional[Callable] = None): @abstractmethod def __hash__(self) -> int: - """Object's unique hash""" + """Object's unique hash.""" @abstractmethod def __eq__(self, other) -> bool: @@ -52,9 +52,7 @@ def _call_func(self, event) -> bool: @abstractmethod def filter(self, event): - """Return True-like value if the event passed the filter and should be - used, or False-like value otherwise. - """ + """Return True-like value if the event passed the filter.""" class RawEvent(EventFilter): @@ -82,31 +80,17 @@ def __eq__(self, other) -> bool: return False def filter(self, event: "AttrDict") -> bool: + """Filter an event. + + Return true if the event should be processed. + """ if self.types and event.kind not in self.types: return False return self._call_func(event) class NewMessage(EventFilter): - """Matches whenever a new message arrives. - - Warning: registering a handler for this event will cause the messages - to be marked as read. Its usage is mainly intended for bots. - - :param pattern: if set, this Pattern will be used to filter the message by its text - content. - :param command: If set, only match messages with the given command (ex. /help). - Setting this property implies `is_info==False`. - :param is_bot: If set to True only match messages sent by bots, if set to None - match messages from bots and users. If omitted or set to False - only messages from users will be matched. - :param is_info: If set to True only match info/system messages, if set to False - only match messages that are not info/system messages. If omitted - info/system messages as well as normal messages will be matched. - :param func: A Callable function that should accept the event as input - parameter, and return a bool value indicating whether the event - should be dispatched or not. - """ + """Matches whenever a new message arrives.""" def __init__( self, @@ -121,6 +105,25 @@ def __init__( is_info: Optional[bool] = None, func: Optional[Callable[["AttrDict"], bool]] = None, ) -> None: + """Initialize a new message filter. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param pattern: if set, this Pattern will be used to filter the message by its text + content. + :param command: If set, only match messages with the given command (ex. /help). + Setting this property implies `is_info==False`. + :param is_bot: If set to True only match messages sent by bots, if set to None + match messages from bots and users. If omitted or set to False + only messages from users will be matched. + :param is_info: If set to True only match info/system messages, if set to False + only match messages that are not info/system messages. If omitted + info/system messages as well as normal messages will be matched. + :param func: A Callable function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ super().__init__(func=func) self.is_bot = is_bot self.is_info = is_info @@ -159,6 +162,7 @@ def __eq__(self, other) -> bool: return False def filter(self, event: "AttrDict") -> bool: + """Return true if if the event is a new message event.""" if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot: return False if self.is_info is not None and self.is_info != event.message_snapshot.is_info: @@ -199,6 +203,7 @@ def __eq__(self, other) -> bool: return False def filter(self, event: "AttrDict") -> bool: + """Return true if if the event is a member addition event.""" if self.added is not None and self.added != event.member_added: return False return self._call_func(event) @@ -231,6 +236,7 @@ def __eq__(self, other) -> bool: return False def filter(self, event: "AttrDict") -> bool: + """Return True if event is matched.""" if self.deleted is not None and self.deleted != event.image_deleted: return False return self._call_func(event) @@ -256,13 +262,12 @@ def __eq__(self, other) -> bool: return False def filter(self, event: "AttrDict") -> bool: + """Return True if event is matched.""" return self._call_func(event) class HookCollection: - """ - Helper class to collect event hooks that can later be added to a Delta Chat client. - """ + """Helper class to collect event hooks that can later be added to a Delta Chat client.""" def __init__(self) -> None: self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 6d1d68ac4f..bc35ffc62e 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,3 +1,5 @@ +"""Message module.""" + import json from dataclasses import dataclass from typing import TYPE_CHECKING, Optional, Union @@ -45,6 +47,7 @@ def get_reactions(self) -> Optional[AttrDict]: return None def get_sender_contact(self) -> Contact: + """Return sender contact.""" from_id = self.get_snapshot().from_id return self.account.get_contact_by_id(from_id) @@ -52,6 +55,14 @@ def mark_seen(self) -> None: """Mark the message as seen.""" self._rpc.markseen_msgs(self.account.id, [self.id]) + def continue_autocrypt_key_transfer(self, setup_code: str) -> None: + """Continue the Autocrypt Setup Message key transfer. + + This function can be called on received Autocrypt Setup Message + to import the key encrypted with the provided setup code. + """ + self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code) + def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: """Send a webxdc status update. This message must be a webxdc.""" if not isinstance(update, str): @@ -59,9 +70,15 @@ def send_webxdc_status_update(self, update: Union[dict, str], description: str) self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: + """Return a list of Webxdc status updates for Webxdc instance message.""" return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) + def get_info(self) -> str: + """Return message info.""" + return self._rpc.get_message_info(self.account.id, self.id) + def get_webxdc_info(self) -> dict: + """Get info from a Webxdc message in JSON format.""" return self._rpc.get_webxdc_info(self.account.id, self.id) def wait_until_delivered(self) -> None: @@ -73,8 +90,10 @@ def wait_until_delivered(self) -> None: @futuremethod def send_webxdc_realtime_advertisement(self): + """Send an advertisement to join the realtime channel.""" yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id) @futuremethod def send_webxdc_realtime_data(self, data) -> None: + """Send data to the realtime channel.""" yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data)) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 63f42b1b3d..31577dbe9c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -1,9 +1,12 @@ +"""Pytest plugin module.""" + from __future__ import annotations import os import random from typing import AsyncGenerator, Optional +import py import pytest from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message @@ -11,55 +14,55 @@ from .rpc import Rpc -def get_temp_credentials() -> dict: - domain = os.getenv("CHATMAIL_DOMAIN") - username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6)) - password = f"{username}${username}" - addr = f"{username}@{domain}" - return {"email": addr, "password": password} - - class ACFactory: + """Test account factory.""" + def __init__(self, deltachat: DeltaChat) -> None: self.deltachat = deltachat def get_unconfigured_account(self) -> Account: + """Create a new unconfigured account.""" account = self.deltachat.add_account() account.set_config("verified_one_on_one_chats", "1") return account def get_unconfigured_bot(self) -> Bot: + """Create a new unconfigured bot.""" return Bot(self.get_unconfigured_account()) - def new_preconfigured_account(self) -> Account: - """Make a new account with configuration options set, but configuration not started.""" - credentials = get_temp_credentials() - account = self.get_unconfigured_account() - account.set_config("addr", credentials["email"]) - account.set_config("mail_pw", credentials["password"]) - assert not account.is_configured() - return account + def get_credentials(self) -> (str, str): + """Generate new credentials for chatmail account.""" + domain = os.getenv("CHATMAIL_DOMAIN") + username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6)) + return f"{username}@{domain}", f"{username}${username}" @futuremethod def new_configured_account(self): - account = self.new_preconfigured_account() - yield account.configure.future() + """Create a new configured account.""" + addr, password = self.get_credentials() + account = self.get_unconfigured_account() + params = {"addr": addr, "password": password} + yield account.add_or_update_transport.future(params) + assert account.is_configured() return account def new_configured_bot(self) -> Bot: - credentials = get_temp_credentials() + """Create a new configured bot.""" + addr, password = self.get_credentials() bot = self.get_unconfigured_bot() - bot.configure(credentials["email"], credentials["password"]) + bot.configure(addr, password) return bot @futuremethod def get_online_account(self): + """Create a new account and start I/O.""" account = yield self.new_configured_account.future() account.bring_online() return account def get_online_accounts(self, num: int) -> list[Account]: + """Create multiple online accounts.""" futures = [self.get_online_account.future() for _ in range(num)] return [f() for f in futures] @@ -74,6 +77,10 @@ def resetup_account(self, ac: Account) -> Account: return ac_clone def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat: + """Create a new 1:1 chat between ac1 and ac2 accepted on both sides. + + Returned chat is a chat with ac2 from ac1 point of view. + """ ac2.create_chat(ac1) return ac1.create_chat(ac2) @@ -85,9 +92,10 @@ def send_message( file: Optional[str] = None, group: Optional[str] = None, ) -> Message: + """Send a message.""" if not from_account: from_account = (self.get_online_accounts(1))[0] - to_contact = from_account.create_contact(to_account.get_config("addr")) + to_contact = from_account.create_contact(to_account) if group: to_chat = from_account.create_group(group) to_chat.add_contact(to_contact) @@ -103,6 +111,7 @@ def process_message( file: Optional[str] = None, group: Optional[str] = None, ) -> AttrDict: + """Send a message and wait until recipient processes it.""" self.send_message( to_account=to_client.account, from_account=from_account, @@ -116,6 +125,7 @@ def process_message( @pytest.fixture def rpc(tmp_path) -> AsyncGenerator: + """RPC client fixture.""" rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts")) with rpc_server: yield rpc_server @@ -123,4 +133,52 @@ def rpc(tmp_path) -> AsyncGenerator: @pytest.fixture def acfactory(rpc) -> AsyncGenerator: + """Return account factory fixture.""" return ACFactory(DeltaChat(rpc)) + + +@pytest.fixture +def data(): + """Test data.""" + + class Data: + def __init__(self) -> None: + for path in reversed(py.path.local(__file__).parts()): + datadir = path.join("test-data") + if datadir.isdir(): + self.path = datadir + return + raise Exception("Data path cannot be found") + + def get_path(self, bn): + """Return path of file or None if it doesn't exist.""" + fn = os.path.join(self.path, *bn.split("/")) + assert os.path.exists(fn) + return fn + + def read_path(self, bn, mode="r"): + fn = self.get_path(bn) + if fn is not None: + with open(fn, mode) as f: + return f.read() + return None + + return Data() + + +@pytest.fixture +def log(): + """Log printer fixture.""" + + class Printer: + def section(self, msg: str) -> None: + print() + print("=" * 10, msg, "=" * 10) + + def step(self, msg: str) -> None: + print("-" * 5, "step " + msg, "-" * 5) + + def indent(self, msg: str) -> None: + print(" " + msg) + + return Printer() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index dcf955ac8e..9ed192925a 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,3 +1,5 @@ +"""JSON-RPC client module.""" + from __future__ import annotations import itertools @@ -12,16 +14,19 @@ class JsonRpcError(Exception): - pass + """JSON-RPC error.""" class RpcFuture: + """RPC future waiting for RPC call result.""" + def __init__(self, rpc: "Rpc", request_id: int, event: Event): self.rpc = rpc self.request_id = request_id self.event = event def __call__(self): + """Wait for the future to return the result.""" self.event.wait() response = self.rpc.request_results.pop(self.request_id) if "error" in response: @@ -32,17 +37,19 @@ def __call__(self): class RpcMethod: + """RPC method.""" + def __init__(self, rpc: "Rpc", name: str): self.rpc = rpc self.name = name def __call__(self, *args) -> Any: - """Synchronously calls JSON-RPC method.""" + """Call JSON-RPC method synchronously.""" future = self.future(*args) return future() def future(self, *args) -> Any: - """Asynchronously calls JSON-RPC method.""" + """Call JSON-RPC method asynchronously.""" request_id = next(self.rpc.id_iterator) request = { "jsonrpc": "2.0", @@ -58,8 +65,13 @@ def future(self, *args) -> Any: class Rpc: + """RPC client.""" + def __init__(self, accounts_dir: Optional[str] = None, **kwargs): - """The given arguments will be passed to subprocess.Popen()""" + """Initialize RPC client. + + The given arguments will be passed to subprocess.Popen(). + """ if accounts_dir: kwargs["env"] = { **kwargs.get("env", os.environ), @@ -81,6 +93,7 @@ def __init__(self, accounts_dir: Optional[str] = None, **kwargs): self.events_thread: Thread def start(self) -> None: + """Start RPC server subprocess.""" if sys.version_info >= (3, 11): self.process = subprocess.Popen( "deltachat-rpc-server", @@ -130,6 +143,7 @@ def __exit__(self, _exc_type, _exc, _tb): self.close() def reader_loop(self) -> None: + """Process JSON-RPC responses from the RPC server process output.""" try: while line := self.process.stdout.readline(): response = json.loads(line) @@ -157,12 +171,13 @@ def writer_loop(self) -> None: logging.exception("Exception in the writer loop") def get_queue(self, account_id: int) -> Queue: + """Get event queue corresponding to the given account ID.""" if account_id not in self.event_queues: self.event_queues[account_id] = Queue() return self.event_queues[account_id] def events_loop(self) -> None: - """Requests new events and distributes them between queues.""" + """Request new events and distributes them between queues.""" try: while True: if self.closing: @@ -178,12 +193,12 @@ def events_loop(self) -> None: logging.exception("Exception in the event loop") def wait_for_event(self, account_id: int) -> Optional[dict]: - """Waits for the next event from the given account and returns it.""" + """Wait for the next event from the given account and returns it.""" queue = self.get_queue(account_id) return queue.get() def clear_all_events(self, account_id: int): - """Removes all queued-up events for a given account. Useful for tests.""" + """Remove all queued-up events for a given account. Useful for tests.""" queue = self.get_queue(account_id) try: while True: diff --git a/deltachat-rpc-client/tests/test_account_events.py b/deltachat-rpc-client/tests/test_account_events.py index 4c4d5c4473..c462f25f43 100644 --- a/deltachat-rpc-client/tests/test_account_events.py +++ b/deltachat-rpc-client/tests/test_account_events.py @@ -13,10 +13,11 @@ def test_event_on_configuration(acfactory: ACFactory) -> None: Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure """ - account = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() account.clear_all_events() assert not account.is_configured() - future = account.configure.future() + future = account.add_or_update_transport.future({"addr": addr, "password": password}) while True: event = account.wait_for_event() if event.kind == EventType.ACCOUNTS_ITEM_CHANGED: diff --git a/deltachat-rpc-client/tests/test_chatlist_events.py b/deltachat-rpc-client/tests/test_chatlist_events.py index 981d4c8d7b..7c9b63466a 100644 --- a/deltachat-rpc-client/tests/test_chatlist_events.py +++ b/deltachat-rpc-client/tests/test_chatlist_events.py @@ -48,8 +48,7 @@ def test_delivery_status(acfactory: ACFactory) -> None: """ alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice.clear_all_events() @@ -119,8 +118,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None: """ alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("hi") @@ -150,18 +148,13 @@ def test_download_on_demand(acfactory: ACFactory) -> None: def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("hi") bob.wait_for_incoming_msg_event() - alice_second_device: Account = acfactory.get_unconfigured_account() - - alice._rpc.provide_backup.future(alice.id) - backup_code = alice._rpc.get_backup_qr(alice.id) - alice_second_device._rpc.get_backup(alice_second_device.id, backup_code) + alice_second_device = alice.clone() alice_second_device.start_io() alice.clear_all_events() alice_second_device.clear_all_events() diff --git a/deltachat-rpc-client/tests/test_iroh_webxdc.py b/deltachat-rpc-client/tests/test_iroh_webxdc.py index 30c24a72a3..c2433d2f76 100644 --- a/deltachat-rpc-client/tests/test_iroh_webxdc.py +++ b/deltachat-rpc-client/tests/test_iroh_webxdc.py @@ -175,17 +175,11 @@ def thread_run(): threading.Thread(target=thread_run, daemon=True).start() - while 1: - event = ac2.wait_for_event() - if event.kind == EventType.WEBXDC_REALTIME_DATA: - n = int(bytes(event.data).decode()) - break + event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA) + n = int(bytes(event.data).decode()) - while 1: - event = ac2.wait_for_event() - if event.kind == EventType.WEBXDC_REALTIME_DATA: - assert int(bytes(event.data).decode()) > n - break + event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA) + assert int(bytes(event.data).decode()) > n def test_no_reordering(acfactory, path_to_webxdc): @@ -229,8 +223,5 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc): ac2_hello_msg_snapshot.chat.accept() ac2_webxdc_msg.send_webxdc_realtime_advertisement() - while 1: - event = ac1.wait_for_event() - if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED: - assert event.msg_id == ac1_webxdc_msg.id - break + event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED) + assert event.msg_id == ac1_webxdc_msg.id diff --git a/deltachat-rpc-client/tests/test_key_transfer.py b/deltachat-rpc-client/tests/test_key_transfer.py new file mode 100644 index 0000000000..2fc2143fa4 --- /dev/null +++ b/deltachat-rpc-client/tests/test_key_transfer.py @@ -0,0 +1,53 @@ +import pytest + +from deltachat_rpc_client import EventType +from deltachat_rpc_client.rpc import JsonRpcError + + +def wait_for_autocrypt_setup_message(account): + while True: + event = account.wait_for_event() + if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0: + msg_id = event.msg_id + msg = account.get_message_by_id(msg_id) + if msg.get_snapshot().is_setupmessage: + return msg + + +def test_autocrypt_setup_message_key_transfer(acfactory): + alice1 = acfactory.get_online_account() + + alice2 = acfactory.get_unconfigured_account() + alice2.set_config("addr", alice1.get_config("addr")) + alice2.set_config("mail_pw", alice1.get_config("mail_pw")) + alice2.configure() + alice2.bring_online() + + setup_code = alice1.initiate_autocrypt_key_transfer() + msg = wait_for_autocrypt_setup_message(alice2) + + # Test that entering wrong code returns an error. + with pytest.raises(JsonRpcError): + msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756") + + msg.continue_autocrypt_key_transfer(setup_code) + + +def test_ac_setup_message_twice(acfactory): + alice1 = acfactory.get_online_account() + + alice2 = acfactory.get_unconfigured_account() + alice2.set_config("addr", alice1.get_config("addr")) + alice2.set_config("mail_pw", alice1.get_config("mail_pw")) + alice2.configure() + alice2.bring_online() + + # Send the first Autocrypt Setup Message and ignore it. + _setup_code = alice1.initiate_autocrypt_key_transfer() + wait_for_autocrypt_setup_message(alice2) + + # Send the second Autocrypt Setup Message and import it. + setup_code = alice1.initiate_autocrypt_key_transfer() + msg = wait_for_autocrypt_setup_message(alice2) + + msg.continue_autocrypt_key_transfer(setup_code) diff --git a/deltachat-rpc-client/tests/test_multidevice.py b/deltachat-rpc-client/tests/test_multidevice.py new file mode 100644 index 0000000000..663542aa08 --- /dev/null +++ b/deltachat-rpc-client/tests/test_multidevice.py @@ -0,0 +1,110 @@ +from imap_tools import AND + +from deltachat_rpc_client import EventType +from deltachat_rpc_client.const import MessageState + + +def test_one_account_send_bcc_setting(acfactory, log, direct_imap): + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_clone = ac1.clone() + ac1_clone.bring_online() + + log.section("send out message without bcc to ourselves") + ac1.set_config("bcc_self", "0") + chat = ac1.create_chat(ac2) + self_addr = ac1.get_config("addr") + other_addr = ac2.get_config("addr") + + msg_out = chat.send_text("message1") + assert not msg_out.get_snapshot().is_forwarded + + # wait for send out (no BCC) + ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT) + assert ac1.get_config("bcc_self") == "0" + + assert self_addr not in ev.msg + assert other_addr in ev.msg + + log.section("ac1: setting bcc_self=1") + ac1.set_config("bcc_self", "1") + + log.section("send out message with bcc to ourselves") + msg_out = chat.send_text("message2") + + # wait for send out (BCC) + ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT) + assert ac1.get_config("bcc_self") == "1" + + # Second client receives only second message, but not the first. + ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED) + assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text + + # now make sure we are sending message to ourselves too + assert self_addr in ev.msg + assert self_addr in ev.msg + + # BCC-self messages are marked as seen by the sender device. + while True: + event = ac1.wait_for_event() + if event.kind == EventType.INFO and event.msg.endswith("Marked messages 1 in folder INBOX as seen."): + break + + # Check that the message is marked as seen on IMAP. + ac1_direct_imap = direct_imap(ac1) + ac1_direct_imap.connect() + ac1_direct_imap.select_folder("Inbox") + assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1 + + +def test_multidevice_sync_seen(acfactory, log): + """Test that message marked as seen on one device is marked as seen on another.""" + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_clone = ac1.clone() + ac1_clone.bring_online() + + ac1.set_config("bcc_self", "1") + ac1_clone.set_config("bcc_self", "1") + + ac1_chat = ac1.create_chat(ac2) + ac1_clone_chat = ac1_clone.create_chat(ac2) + ac2_chat = ac2.create_chat(ac1) + + log.section("Send a message from ac2 to ac1 and check that it's 'fresh'") + ac2_chat.send_text("Hi") + ac1_message = ac1.wait_for_incoming_msg() + ac1_clone_message = ac1_clone.wait_for_incoming_msg() + assert ac1_chat.get_fresh_message_count() == 1 + assert ac1_clone_chat.get_fresh_message_count() == 1 + assert ac1_message.get_snapshot().state == MessageState.IN_FRESH + assert ac1_clone_message.get_snapshot().state == MessageState.IN_FRESH + + log.section("ac1 marks message as seen on the first device") + ac1.mark_seen_messages([ac1_message]) + assert ac1_message.get_snapshot().state == MessageState.IN_SEEN + + log.section("ac1 clone detects that message is marked as seen") + ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED) + assert ev.chat_id == ac1_clone_chat.id + + log.section("Send an ephemeral message from ac2 to ac1") + ac2_chat.set_ephemeral_timer(60) + ac1.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED) + ac1.wait_for_incoming_msg() + ac1_clone.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED) + ac1_clone.wait_for_incoming_msg() + + ac2_chat.send_text("Foobar") + ac1_message = ac1.wait_for_incoming_msg() + ac1_clone_message = ac1_clone.wait_for_incoming_msg() + assert "Ephemeral timer: 60\n" in ac1_message.get_info() + assert "Expires: " not in ac1_clone_message.get_info() + assert "Ephemeral timer: 60\n" in ac1_message.get_info() + assert "Expires: " not in ac1_clone_message.get_info() + + ac1_message.mark_seen() + assert "Expires: " in ac1_message.get_info() + ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED) + assert ev.chat_id == ac1_clone_chat.id + assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN + # Test that the timer is started on the second device after synchronizing the seen status. + assert "Expires: " in ac1_clone_message.get_info() diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 17b17ec1e4..03ade4d04e 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -4,6 +4,7 @@ import pytest from deltachat_rpc_client import Chat, EventType, SpecialContactId +from deltachat_rpc_client.rpc import JsonRpcError def test_qr_setup_contact(acfactory, tmp_path) -> None: @@ -26,17 +27,21 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None: bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() assert bob_contact_alice_snapshot.is_verified - # Test that if Bob changes the key, backwards verification is lost. + # Test that if Bob imports a key, + # backwards verification is not lost + # because default key is not changed. logging.info("Bob 2 is created") bob2 = acfactory.new_configured_account() bob2.export_self_keys(tmp_path) - logging.info("Bob imports a key") - bob.import_self_keys(tmp_path) + logging.info("Bob tries to import a key") + # Importing a second key is not allowed. + with pytest.raises(JsonRpcError): + bob.import_self_keys(tmp_path) - assert bob.get_config("key_id") == "2" + assert bob.get_config("key_id") == "1" bob_contact_alice_snapshot = bob_contact_alice.get_snapshot() - assert not bob_contact_alice_snapshot.is_verified + assert bob_contact_alice_snapshot.is_verified def test_qr_setup_contact_svg(acfactory) -> None: @@ -55,15 +60,12 @@ def test_qr_setup_contact_svg(acfactory) -> None: @pytest.mark.parametrize("protect", [True, False]) -def test_qr_securejoin(acfactory, protect, tmp_path): +def test_qr_securejoin(acfactory, protect): alice, bob, fiona = acfactory.get_online_accounts(3) # Setup second device for Alice # to test observing securejoin protocol. - alice.export_backup(tmp_path) - files = list(tmp_path.glob("*.tar")) - alice2 = acfactory.get_unconfigured_account() - alice2.import_backup(files[0]) + alice2 = alice.clone() logging.info("Alice creates a group") alice_chat = alice.create_group("Group", protect=protect) @@ -74,17 +76,11 @@ def test_qr_securejoin(acfactory, protect, tmp_path): bob.secure_join(qr_code) # Alice deletes "vg-request". - while True: - event = alice.wait_for_event() - if event["kind"] == "ImapMessageDeleted": - break + alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED) alice.wait_for_securejoin_inviter_success() # Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth". for ac in [alice, bob]: - while True: - event = ac.wait_for_event() - if event["kind"] == "ImapMessageDeleted": - break + ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED) bob.wait_for_securejoin_joiner_success() # Test that Alice verified Bob's profile. @@ -93,7 +89,7 @@ def test_qr_securejoin(acfactory, protect, tmp_path): assert alice_contact_bob_snapshot.is_verified snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr")) + assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr")) assert snapshot.chat.get_basic_snapshot().is_protected == protect # Test that Bob verified Alice's profile. @@ -121,8 +117,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None: """Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode.""" alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("Hello!") @@ -159,11 +154,8 @@ def test_qr_readreceipt(acfactory) -> None: logging.info("Alice creates a verified group") group = alice.create_group("Group", protect=True) - bob_addr = bob.get_config("addr") - charlie_addr = charlie.get_config("addr") - - alice_contact_bob = alice.create_contact(bob_addr, "Bob") - alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie") + alice_contact_bob = alice.create_contact(bob, "Bob") + alice_contact_charlie = alice.create_contact(charlie, "Charlie") group.add_contact(alice_contact_bob) group.add_contact(alice_contact_charlie) @@ -190,7 +182,7 @@ def test_qr_readreceipt(acfactory) -> None: charlie_snapshot = charlie_message.get_snapshot() assert charlie_snapshot.text == "Hi from Bob!" - bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie") + bob_contact_charlie = bob.create_contact(charlie, "Charlie") assert not bob.get_chat_by_contact(bob_contact_charlie) logging.info("Charlie reads Bob's message") @@ -461,12 +453,12 @@ def test_qr_new_group_unblocked(acfactory): assert ac2_msg.chat.get_basic_snapshot().is_contact_request +@pytest.mark.skip(reason="AEAP is disabled for now") def test_aeap_flow_verified(acfactory): """Test that a new address is added to a contact when it changes its address.""" ac1, ac2 = acfactory.get_online_accounts(2) - # ac1new is only used to get a new address. - ac1new = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() logging.info("ac1: create verified-group QR, ac2 scans and joins") chat = ac1.create_group("hello", protect=True) @@ -486,8 +478,8 @@ def test_aeap_flow_verified(acfactory): assert msg_in_1.text == msg_out.text logging.info("changing email account") - ac1.set_config("addr", ac1new.get_config("addr")) - ac1.set_config("mail_pw", ac1new.get_config("mail_pw")) + ac1.set_config("addr", addr) + ac1.set_config("mail_pw", password) ac1.stop_io() ac1.configure() ac1.start_io() @@ -500,11 +492,9 @@ def test_aeap_flow_verified(acfactory): msg_in_2_snapshot = msg_in_2.get_snapshot() assert msg_in_2_snapshot.text == msg_out.text assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id - assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr") + assert msg_in_2.get_sender_contact().get_snapshot().address == addr assert len(msg_in_2_snapshot.chat.get_contacts()) == 2 - assert ac1new.get_config("addr") in [ - contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts() - ] + assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()] def test_gossip_verification(acfactory) -> None: @@ -520,9 +510,9 @@ def test_gossip_verification(acfactory) -> None: bob.secure_join(qr_code) bob.wait_for_securejoin_joiner_success() - bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice") - bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol") - carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice") + bob_contact_alice = bob.create_contact(alice, "Alice") + bob_contact_carol = bob.create_contact(carol, "Carol") + carol_contact_alice = carol.create_contact(alice, "Alice") logging.info("Bob creates an Autocrypt group") bob_group_chat = bob.create_group("Autocrypt Group") @@ -573,7 +563,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: # ac1 waits for member added message and creates a QR code. snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr")) + assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr")) ac1_qr_code = snapshot.chat.get_qr_code() # ac2 verifies ac1 @@ -582,7 +572,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: ac2.wait_for_securejoin_joiner_success() # ac1 is verified for ac2. - ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "") + ac2_contact_ac1 = ac2.create_contact(ac1, "") assert ac2_contact_ac1.get_snapshot().is_verified # ac1 resetups the account. @@ -597,7 +587,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None: # header sent by old ac1. while True: # ac1 sends a message to ac2. - ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "") + ac1_contact_ac2 = ac1.create_contact(ac2, "") ac1_chat_ac2 = ac1_contact_ac2.create_chat() ac1_chat_ac2.send_text("Hello!") @@ -653,12 +643,14 @@ def test_withdraw_securejoin_qr(acfactory): bob_chat = bob.secure_join(qr_code) bob.wait_for_securejoin_joiner_success() + alice.clear_all_events() + snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() - assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr")) + assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr")) assert snapshot.chat.get_basic_snapshot().is_protected bob_chat.leave() - snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot() + snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot() assert snapshot.text == "Group left by {}.".format(bob.get_config("addr")) logging.info("Alice withdraws QR code.") diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index f91ce90761..d859140f71 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -61,61 +61,102 @@ def test_acfactory(acfactory) -> None: def test_configure_starttls(acfactory) -> None: - account = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account.add_or_update_transport( + { + "addr": addr, + "password": password, + "imapSecurity": "starttls", + "smtpSecurity": "starttls", + }, + ) + assert account.is_configured() + - # Use STARTTLS - account.set_config("mail_security", "2") - account.set_config("send_security", "2") - account.configure() +def test_lowercase_address(acfactory) -> None: + addr, password = acfactory.get_credentials() + addr_upper = addr.upper() + account = acfactory.get_unconfigured_account() + account.add_or_update_transport( + { + "addr": addr_upper, + "password": password, + }, + ) assert account.is_configured() + assert addr_upper != addr + assert account.get_config("configured_addr") == addr + assert account.list_transports()[0]["addr"] == addr + for param in [ + account.get_info()["used_account_settings"], + account.get_info()["entered_account_settings"], + ]: + assert addr in param + assert addr_upper not in param -def test_configure_ip(acfactory) -> None: - account = acfactory.new_preconfigured_account() - domain = account.get_config("addr").rsplit("@")[-1] - ip_address = socket.gethostbyname(domain) +def test_configure_ip(acfactory) -> None: + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + ip_address = socket.gethostbyname(addr.rsplit("@")[-1]) - # This should fail TLS check. - account.set_config("mail_server", ip_address) with pytest.raises(JsonRpcError): - account.configure() + account.add_or_update_transport( + { + "addr": addr, + "password": password, + # This should fail TLS check. + "imapServer": ip_address, + }, + ) def test_configure_alternative_port(acfactory) -> None: """Test that configuration with alternative port 443 works.""" - account = acfactory.new_preconfigured_account() - - account.set_config("mail_port", "443") - account.set_config("send_port", "443") - - account.configure() - - -def test_configure_username(acfactory) -> None: - account = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account.add_or_update_transport( + { + "addr": addr, + "password": password, + "imapPort": 443, + "smtpPort": 443, + }, + ) + assert account.is_configured() - addr = account.get_config("addr") - account.set_config("mail_user", addr) - account.configure() - assert account.get_config("configured_mail_user") == addr +def test_list_transports(acfactory) -> None: + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account.add_or_update_transport( + { + "addr": addr, + "password": password, + "imapUser": addr, + }, + ) + transports = account.list_transports() + assert len(transports) == 1 + params = transports[0] + assert params["addr"] == addr + assert params["password"] == password + assert params["imapUser"] == addr def test_account(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("Hello!") - while True: - event = bob.wait_for_event() - if event.kind == EventType.INCOMING_MSG: - chat_id = event.chat_id - msg_id = event.msg_id - break + event = bob.wait_for_incoming_msg_event() + chat_id = event.chat_id + msg_id = event.msg_id message = bob.get_message_by_id(msg_id) snapshot = message.get_snapshot() @@ -174,8 +215,7 @@ def test_account(acfactory) -> None: def test_chat(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("Hello!") @@ -241,7 +281,7 @@ def test_contact(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert repr(alice_contact_bob) @@ -258,8 +298,7 @@ def test_contact(acfactory) -> None: def test_message(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_text("Hello!") @@ -287,28 +326,74 @@ def test_message(acfactory) -> None: assert reactions == snapshot.reactions +def test_selfavatar_sync(acfactory, data, log) -> None: + alice = acfactory.get_online_account() + + log.section("Alice adds a second device") + alice2 = alice.clone() + + log.section("Second device goes online") + alice2.start_io() + + log.section("First device changes avatar") + image = data.get_path("image/avatar1000x1000.jpg") + alice.set_config("selfavatar", image) + avatar_config = alice.get_config("selfavatar") + avatar_hash = os.path.basename(avatar_config) + print("Info: avatar hash is ", avatar_hash) + + log.section("First device receives avatar change") + alice2.wait_for_event(EventType.SELFAVATAR_CHANGED) + avatar_config2 = alice2.get_config("selfavatar") + avatar_hash2 = os.path.basename(avatar_config2) + print("Info: avatar hash on second device is ", avatar_hash2) + assert avatar_hash == avatar_hash2 + assert avatar_config != avatar_config2 + + +def test_reaction_seen_on_another_dev(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) + alice2 = alice.clone() + alice2.start_io() + + alice_contact_bob = alice.create_contact(bob, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") + + event = bob.wait_for_incoming_msg_event() + msg_id = event.msg_id + + message = bob.get_message_by_id(msg_id) + snapshot = message.get_snapshot() + snapshot.chat.accept() + message.send_reaction("😎") + for a in [alice, alice2]: + a.wait_for_event(EventType.INCOMING_REACTION) + + alice2.clear_all_events() + alice_chat_bob.mark_noticed() + chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id + alice2_chat_bob = alice2.create_chat(bob) + assert chat_id == alice2_chat_bob.id + + def test_is_bot(acfactory) -> None: """Test that we can recognize messages submitted by bots.""" alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() # Alice becomes a bot. alice.set_config("bot", "1") alice_chat_bob.send_text("Hello!") - while True: - event = bob.wait_for_event() - if event.kind == EventType.INCOMING_MSG: - msg_id = event.msg_id - message = bob.get_message_by_id(msg_id) - snapshot = message.get_snapshot() - assert snapshot.chat_id == event.chat_id - assert snapshot.text == "Hello!" - assert snapshot.is_bot - break + event = bob.wait_for_incoming_msg_event() + message = bob.get_message_by_id(event.msg_id) + snapshot = message.get_snapshot() + assert snapshot.chat_id == event.chat_id + assert snapshot.text == "Hello!" + assert snapshot.is_bot def test_bot(acfactory) -> None: @@ -355,9 +440,11 @@ def test_wait_next_messages(acfactory) -> None: alice = acfactory.new_configured_account() # Create a bot account so it does not receive device messages in the beginning. - bot = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + bot = acfactory.get_unconfigured_account() bot.set_config("bot", "1") - bot.configure() + bot.add_or_update_transport({"addr": addr, "password": password}) + assert bot.is_configured() # There are no old messages and the call returns immediately. assert not bot.wait_next_messages() @@ -366,8 +453,7 @@ def test_wait_next_messages(acfactory) -> None: # Bot starts waiting for messages. next_messages_task = executor.submit(bot.wait_next_messages) - bot_addr = bot.get_config("addr") - alice_contact_bot = alice.create_contact(bot_addr, "Bot") + alice_contact_bot = alice.create_contact(bot, "Bot") alice_chat_bot = alice_contact_bot.create_chat() alice_chat_bot.send_text("Hello!") @@ -391,9 +477,7 @@ def test_import_export_backup(acfactory, tmp_path) -> None: def test_import_export_keys(acfactory, tmp_path) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") - alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob = alice.create_chat(bob) alice_chat_bob.send_text("Hello Bob!") snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() @@ -443,9 +527,7 @@ def test_provider_info(rpc) -> None: def test_mdn_doesnt_break_autocrypt(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") # Bob creates chat manually so chat with Alice is accepted. alice_chat_bob = alice_contact_bob.create_chat() @@ -469,10 +551,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None: # Alice reads Bob's message. message.mark_seen() - while True: - event = bob.wait_for_event() - if event.kind == EventType.MSG_READ: - break + bob.wait_for_event(EventType.MSG_READ) # Bob sends a message to Alice, it should also be encrypted. bob_chat_alice.send_text("Hi Alice!") @@ -544,9 +623,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap): messages they refer to and thus dropped. """ (ac1,) = acfactory.get_online_accounts(1) - ac2 = acfactory.new_preconfigured_account() - ac2.configure() + + addr, password = acfactory.get_credentials() + ac2 = acfactory.get_unconfigured_account() + ac2.add_or_update_transport({"addr": addr, "password": password}) ac2.set_config("mvbox_move", "1") + assert ac2.is_configured() + ac2.bring_online() chat1 = acfactory.get_accepted_chat(ac1, ac2) ac2.stop_io() @@ -590,9 +673,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts): chat.send_text("Hello Alice!") assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!" - contact_addr = account.get_config("addr") - contact = alice.create_contact(contact_addr, "") - + contact = alice.create_contact(account) alice_group.add_contact(contact) if n_accounts == 2: @@ -623,7 +704,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts): assert snapshot.chat == bob_chat_alice -def test_markseen_contact_request(acfactory, tmp_path): +def test_markseen_contact_request(acfactory): """ Test that seen status is synchronized for contact request messages even though read receipt is not sent. @@ -631,10 +712,7 @@ def test_markseen_contact_request(acfactory, tmp_path): alice, bob = acfactory.get_online_accounts(2) # Bob sets up a second device. - bob.export_backup(tmp_path) - files = list(tmp_path.glob("*.tar")) - bob2 = acfactory.get_unconfigured_account() - bob2.import_backup(files[0]) + bob2 = bob.clone() bob2.start_io() alice_chat_bob = alice.create_chat(bob) @@ -645,10 +723,7 @@ def test_markseen_contact_request(acfactory, tmp_path): assert message2.get_snapshot().state == MessageState.IN_FRESH message.mark_seen() - while True: - event = bob2.wait_for_event() - if event.kind == EventType.MSGS_NOTICED: - break + bob2.wait_for_event(EventType.MSGS_NOTICED) assert message2.get_snapshot().state == MessageState.IN_SEEN @@ -661,12 +736,11 @@ def test_get_http_response(acfactory): def test_configured_imap_certificate_checks(acfactory): alice = acfactory.new_configured_account() - configured_certificate_checks = alice.get_config("configured_imap_certificate_checks") # Certificate checks should be configured (not None) - assert configured_certificate_checks + assert "cert_automatic" in alice.get_info().used_account_settings - # 0 is the value old Delta Chat core versions used + # "cert_old_automatic" is the value old Delta Chat core versions used # to mean user entered "imap_certificate_checks=0" (Automatic) # and configuration failed to use strict TLS checks # so it switched strict TLS checks off. @@ -677,4 +751,99 @@ def test_configured_imap_certificate_checks(acfactory): # # Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug. # This test is a regression test to prevent this happening again. - assert configured_certificate_checks != "0" + assert "cert_old_automatic" not in alice.get_info().used_account_settings + + +def test_no_old_msg_is_fresh(acfactory): + ac1, ac2 = acfactory.get_online_accounts(2) + ac1_clone = ac1.clone() + ac1_clone.start_io() + + ac1.create_chat(ac2) + ac1_clone_chat = ac1_clone.create_chat(ac2) + + ac1.get_device_chat().mark_noticed() + + logging.info("Send a first message from ac2 to ac1 and check that it's 'fresh'") + first_msg = ac2.create_chat(ac1).send_text("Hi") + ac1.wait_for_incoming_msg_event() + assert ac1.create_chat(ac2).get_fresh_message_count() == 1 + assert len(list(ac1.get_fresh_messages())) == 1 + + ac1.wait_for_event(EventType.IMAP_INBOX_IDLE) + + logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'") + ac1_clone_chat.send_text("Hi back") + ev = ac1.wait_for_msgs_noticed_event() + + assert ev.chat_id == first_msg.get_snapshot().chat_id + assert ac1.create_chat(ac2).get_fresh_message_count() == 0 + assert len(list(ac1.get_fresh_messages())) == 0 + + +def test_rename_synchronization(acfactory): + """Test synchronization of contact renaming.""" + alice, bob = acfactory.get_online_accounts(2) + alice2 = alice.clone() + alice2.bring_online() + + bob.set_config("displayname", "Bob") + bob.create_chat(alice).send_text("Hello!") + alice_msg = alice.wait_for_incoming_msg().get_snapshot() + alice2_msg = alice2.wait_for_incoming_msg().get_snapshot() + + assert alice2_msg.sender.get_snapshot().display_name == "Bob" + alice_msg.sender.set_name("Bobby") + alice2.wait_for_event(EventType.CONTACTS_CHANGED) + assert alice2_msg.sender.get_snapshot().display_name == "Bobby" + + +def test_rename_group(acfactory): + """Test renaming the group.""" + alice, bob = acfactory.get_online_accounts(2) + + alice_group = alice.create_group("Test group") + alice_contact_bob = alice.create_contact(bob) + alice_group.add_contact(alice_contact_bob) + alice_group.send_text("Hello!") + + bob_msg = bob.wait_for_incoming_msg() + bob_chat = bob_msg.get_snapshot().chat + assert bob_chat.get_basic_snapshot().name == "Test group" + + for name in ["Baz", "Foo bar", "Xyzzy"]: + alice_group.set_name(name) + bob.wait_for_incoming_msg_event() + assert bob_chat.get_basic_snapshot().name == name + + +def test_get_all_accounts_deadlock(rpc): + """Regression test for get_all_accounts deadlock.""" + for _ in range(100): + all_accounts = rpc.get_all_accounts.future() + rpc.add_account() + all_accounts() + + +def test_delete_deltachat_folder(acfactory, direct_imap): + """Test that DeltaChat folder is recreated if user deletes it manually.""" + ac1 = acfactory.new_configured_account() + ac1.set_config("mvbox_move", "1") + ac1.bring_online() + + ac1_direct_imap = direct_imap(ac1) + ac1_direct_imap.conn.folder.delete("DeltaChat") + assert "DeltaChat" not in ac1_direct_imap.list_folders() + + # Wait until new folder is created and UIDVALIDITY is updated. + while True: + event = ac1.wait_for_event() + if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg: + break + + ac2 = acfactory.get_online_account() + ac2.create_chat(ac1).send_text("hello") + msg = ac1.wait_for_incoming_msg().get_snapshot() + assert msg.text == "hello" + + assert "DeltaChat" in ac1_direct_imap.list_folders() diff --git a/deltachat-rpc-client/tests/test_vcard.py b/deltachat-rpc-client/tests/test_vcard.py index cb4d954021..53e8348b33 100644 --- a/deltachat-rpc-client/tests/test_vcard.py +++ b/deltachat-rpc-client/tests/test_vcard.py @@ -1,8 +1,7 @@ def test_vcard(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie") alice_chat_bob = alice_contact_bob.create_chat() diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py index e40d169716..314e8f6747 100644 --- a/deltachat-rpc-client/tests/test_webxdc.py +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -1,20 +1,13 @@ -from deltachat_rpc_client import EventType - - def test_webxdc(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") - while True: - event = bob.wait_for_event() - if event.kind == EventType.INCOMING_MSG: - bob_chat_alice = bob.get_chat_by_id(event.chat_id) - message = bob.get_message_by_id(event.msg_id) - break + event = bob.wait_for_incoming_msg_event() + bob_chat_alice = bob.get_chat_by_id(event.chat_id) + message = bob.get_message_by_id(event.msg_id) webxdc_info = message.get_webxdc_info() assert webxdc_info == { @@ -51,8 +44,7 @@ def test_webxdc(acfactory) -> None: def test_webxdc_insert_lots_of_updates(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2) - bob_addr = bob.get_config("addr") - alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_contact_bob = alice.create_contact(bob, "Bob") alice_chat_bob = alice_contact_bob.create_chat() message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini index 2c3b0cb4ef..2ad52b8f52 100644 --- a/deltachat-rpc-client/tox.ini +++ b/deltachat-rpc-client/tox.ini @@ -12,11 +12,8 @@ setenv = RUST_MIN_STACK=8388608 passenv = CHATMAIL_DOMAIN -deps = - pytest - pytest-timeout - pytest-xdist - imap-tools +dependency_groups = + dev [testenv:lint] skipsdist = True @@ -24,7 +21,7 @@ skip_install = True deps = ruff commands = - ruff format --quiet --diff src/ examples/ tests/ + ruff format --diff src/ examples/ tests/ ruff check src/ examples/ tests/ [pytest] diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index abae5611e7..f5678301cd 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.155.2" +version = "1.159.5" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/deltachat-rpc-server/README.md b/deltachat-rpc-server/README.md index 32ea83ac7b..a5108c4c8d 100644 --- a/deltachat-rpc-server/README.md +++ b/deltachat-rpc-server/README.md @@ -5,13 +5,13 @@ over standard I/O. ## Install -To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases). +To download binary pre-builds check the [releases page](https://github.com/chatmail/core/releases). Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`. To install from source run: ```sh -cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server +cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server ``` The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available diff --git a/deltachat-rpc-server/npm-package/package.json b/deltachat-rpc-server/npm-package/package.json index 0b664671f1..0ea58135d6 100644 --- a/deltachat-rpc-server/npm-package/package.json +++ b/deltachat-rpc-server/npm-package/package.json @@ -8,12 +8,12 @@ }, "repository": { "type": "git", - "url": "https://github.com/deltachat/deltachat-core-rust.git" + "url": "https://github.com/chatmail/core.git" }, "scripts": { "prepack": "node scripts/update_optional_dependencies_and_version.js" }, "type": "module", "types": "index.d.ts", - "version": "1.155.2" + "version": "1.159.5" } diff --git a/deltachat-rpc-server/npm-package/scripts/src/make_package.py b/deltachat-rpc-server/npm-package/scripts/src/make_package.py index 0e9cc528a1..6400d04673 100644 --- a/deltachat-rpc-server/npm-package/scripts/src/make_package.py +++ b/deltachat-rpc-server/npm-package/scripts/src/make_package.py @@ -25,7 +25,7 @@ def write_package_json(platform_path, rust_target, my_binary_name): "license": "MPL-2.0", "repository": { "type": "git", - "url": "https://github.com/deltachat/deltachat-core-rust.git", + "url": "https://github.com/chatmail/core.git", }, } diff --git a/deltachat-rpc-server/npm-package/src/errors.js b/deltachat-rpc-server/npm-package/src/errors.js index 52d8f115aa..92f23c1946 100644 --- a/deltachat-rpc-server/npm-package/src/errors.js +++ b/deltachat-rpc-server/npm-package/src/errors.js @@ -2,7 +2,7 @@ import { ENV_VAR_NAME } from "./const.js"; const cargoInstallCommand = - "cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server"; + "cargo install --git https://github.com/chatmail/core deltachat-rpc-server"; export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) { return `deltachat-rpc-server not found: diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs index 171edd3c11..66f9da93a4 100644 --- a/deltachat-rpc-server/src/main.rs +++ b/deltachat-rpc-server/src/main.rs @@ -30,7 +30,7 @@ async fn main() { // thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang // until the user presses enter." if let Err(error) = &r { - log::error!("Fatal error: {error:#}.") + log::error!("Error: {error:#}.") } std::process::exit(if r.is_ok() { 0 } else { 1 }); } diff --git a/deny.toml b/deny.toml index 7f5e765503..5910ebc5f1 100644 --- a/deny.toml +++ b/deny.toml @@ -1,7 +1,5 @@ [advisories] ignore = [ - "RUSTSEC-2020-0071", - # Timing attack on RSA. # Delta Chat does not use RSA for new keys # and this requires precise measurement of the decryption time by the attacker. @@ -9,11 +7,11 @@ ignore = [ # "RUSTSEC-2023-0071", - # Unmaintained encoding - "RUSTSEC-2021-0153", - # Unmaintained instant "RUSTSEC-2024-0384", + + # Unmaintained paste + "RUSTSEC-2024-0436", ] [bans] @@ -23,39 +21,31 @@ ignore = [ # Please keep this list alphabetically sorted. skip = [ { name = "async-channel", version = "1.9.0" }, - { name = "base64", version = "<0.21" }, - { name = "base64", version = "0.21.7" }, { name = "bitflags", version = "1.3.2" }, { name = "event-listener", version = "2.5.3" }, - { name = "fastrand", version = "1.9.0" }, - { name = "fiat-crypto", version = "0.1.20" }, - { name = "futures-lite", version = "1.13.0" }, { name = "generator", version = "0.7.5" }, - { name = "getrandom", version = "<0.2" }, - { name = "hostname", version = "0.3.1" }, + { name = "getrandom", version = "0.2.12" }, + { name = "heck", version = "0.4.1" }, { name = "http", version = "0.2.12" }, - { name = "iroh-metrics", version = "0.30.0" }, + { name = "linux-raw-sys", version = "0.4.14" }, { name = "loom", version = "0.5.6" }, { name = "netlink-packet-route", version = "0.17.1" }, - { name = "netlink-packet-route", version = "0.21.0" }, - { name = "netwatch" }, - { name = "nix", version = "0.26.4" }, - { name = "nix", version = "0.27.1" }, - { name = "quick-error", version = "<2.0" }, - { name = "rand_chacha", version = "<0.3" }, - { name = "rand_core", version = "<0.6" }, - { name = "rand", version = "<0.8" }, + { name = "nom", version = "7.1.3" }, + { name = "rand_chacha", version = "0.3.1" }, + { name = "rand_core", version = "0.6.4" }, + { name = "rand", version = "0.8.5" }, { name = "redox_syscall", version = "0.3.5" }, { name = "regex-automata", version = "0.1.10" }, { name = "regex-syntax", version = "0.6.29" }, - { name = "rtnetlink", version = "0.13.1" }, - { name = "sync_wrapper", version = "0.1.2" }, + { name = "rustix", version = "0.38.44" }, + { name = "spin", version = "0.9.8" }, + { name = "strum_macros", version = "0.26.2" }, + { name = "strum", version = "0.26.2" }, { name = "syn", version = "1.0.109" }, + { name = "lru", version = "0.12.3" }, { name = "thiserror-impl", version = "1.0.69" }, { name = "thiserror", version = "1.0.69" }, - { name = "time", version = "<0.3" }, - { name = "unicode-width", version = "0.1.11" }, - { name = "wasi", version = "<0.11" }, + { name = "wasi", version = "0.11.0+wasi-snapshot-preview1" }, { name = "windows" }, { name = "windows_aarch64_gnullvm" }, { name = "windows_aarch64_msvc" }, @@ -72,6 +62,7 @@ skip = [ { name = "windows_x86_64_gnu" }, { name = "windows_x86_64_gnullvm" }, { name = "windows_x86_64_msvc" }, + { name = "zerocopy", version = "0.7.32" }, ] @@ -86,9 +77,9 @@ allow = [ "ISC", "MIT", "MPL-2.0", - "OpenSSL", "Unicode-3.0", "Unicode-DFS-2016", + "Unlicense", "Zlib", ] @@ -98,9 +89,3 @@ expression = "MIT AND ISC AND OpenSSL" license-files = [ { path = "LICENSE", hash = 0xbd0eed23 }, ] - -[sources.allow-org] -# Organisations which we allow git sources from. -github = [ - "deltachat", -] diff --git a/flake.lock b/flake.lock index feb520342a..e78a0e19b2 100644 --- a/flake.lock +++ b/flake.lock @@ -47,11 +47,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1737527504, - "narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=", + "lastModified": 1747291057, + "narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=", "owner": "nix-community", "repo": "fenix", - "rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d", + "rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f", "type": "github" }, "original": { @@ -147,11 +147,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1737469691, - "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=", + "lastModified": 1747179050, + "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab", + "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "type": "github" }, "original": { @@ -175,11 +175,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1731139594, - "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", + "lastModified": 1747179050, + "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", + "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "type": "github" }, "original": { @@ -202,11 +202,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1737453499, - "narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=", + "lastModified": 1746889290, + "narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "0b68402d781955d526b80e5d479e9e47addb4075", + "rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e5d232f303..527c463a65 100644 --- a/flake.nix +++ b/flake.nix @@ -18,9 +18,9 @@ manifest = (pkgs.lib.importTOML ./Cargo.toml).package; androidSdk = android.sdk.${system} (sdkPkgs: builtins.attrValues { - inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest; + inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest; }); - androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837"; + androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018"; rustSrc = nix-filter.lib { root = ./.; @@ -30,6 +30,7 @@ include = [ ./benches ./assets + ./fuzz ./Cargo.lock ./Cargo.toml ./CMakeLists.txt @@ -87,10 +88,6 @@ }; cargoLock = { lockFile = ./Cargo.lock; - outputHashes = { - "email-0.0.20" = "sha256-cfR3D5jFQpw32bGsgapK2Uwuxmht+rRK/n1ZUmCb2WA="; - "lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk="; - }; }; mkRustPackage = packageName: naersk'.buildPackage { @@ -312,10 +309,41 @@ LD = "${targetCc}"; }; - mkAndroidPackages = arch: { - "deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server"; - "deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl"; - }; + mkAndroidPackages = arch: + let + rpc-server = mkAndroidRustPackage arch "deltachat-rpc-server"; + in + { + "deltachat-rpc-server-${arch}-android" = rpc-server; + "deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl"; + "deltachat-rpc-server-${arch}-android-wheel" = + pkgs.stdenv.mkDerivation { + pname = "deltachat-rpc-server-${arch}-android-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}-android tmp/deltachat-rpc-server + ''; + installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out''; + }; + }; mkRustPackages = arch: let @@ -556,6 +584,9 @@ cargo-nextest perl # needed to build vendored OpenSSL git-cliff + (python3.withPackages (pypkgs: with pypkgs; [ + tox + ])) ]; }; } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock deleted file mode 100644 index 13489ec300..0000000000 --- a/fuzz/Cargo.lock +++ /dev/null @@ -1,6836 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "bytes", - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aes-kw" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" -dependencies = [ - "aes", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.11", - "once_cell", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -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-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", - "zeroize", -] - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "ascii_utils" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" - -[[package]] -name = "asn1-rs" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 1.0.58", - "time 0.3.36", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", - "synstructure 0.13.1", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "async-broadcast" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" -dependencies = [ - "event-listener 5.3.1", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" -dependencies = [ - "flate2", - "futures-core", - "futures-io", - "memchr", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-imap" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5488cd022c3c7bc41a9b34a540d9ac0d9c5cd42fdb106a67616521b7592d5b4e" -dependencies = [ - "async-channel 2.3.1", - "async-compression", - "base64 0.21.7", - "bytes", - "chrono", - "futures", - "imap-proto", - "log", - "nom", - "once_cell", - "pin-project", - "pin-utils", - "self_cell", - "stop-token", - "thiserror 1.0.58", - "tokio", -] - -[[package]] -name = "async-native-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" -dependencies = [ - "native-tls", - "thiserror 1.0.58", - "tokio", - "url", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "async-smtp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee04bcf0a7ebf5594f9aff84935dc8cb0490b65055913a7a4c4d08f81e181d6" -dependencies = [ - "anyhow", - "base64 0.13.1", - "futures", - "log", - "nom", - "pin-project", - "thiserror 1.0.58", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "async_zip" -version = "0.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" -dependencies = [ - "async-compression", - "crc32fast", - "futures-lite 2.5.0", - "pin-project", - "thiserror 1.0.58", - "tokio", - "tokio-util", -] - -[[package]] -name = "attohttpc" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" -dependencies = [ - "http 0.2.12", - "log", - "url", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.11", - "instant", - "rand 0.8.5", -] - -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" - -[[package]] -name = "bitfield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "blake3" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blowfish" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" -dependencies = [ - "byteorder", - "cipher", -] - -[[package]] -name = "bolero" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c" -dependencies = [ - "bolero-afl", - "bolero-engine", - "bolero-generator", - "bolero-honggfuzz", - "bolero-kani", - "bolero-libfuzzer", - "cfg-if", - "rand 0.8.5", -] - -[[package]] -name = "bolero-afl" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89" -dependencies = [ - "bolero-engine", - "cc", -] - -[[package]] -name = "bolero-engine" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2" -dependencies = [ - "anyhow", - "backtrace", - "bolero-generator", - "lazy_static", - "pretty-hex", - "rand 0.8.5", -] - -[[package]] -name = "bolero-generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb" -dependencies = [ - "bolero-generator-derive", - "either", - "rand_core 0.6.4", -] - -[[package]] -name = "bolero-generator-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "bolero-honggfuzz" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355" -dependencies = [ - "bolero-engine", -] - -[[package]] -name = "bolero-kani" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e" -dependencies = [ - "bolero-engine", -] - -[[package]] -name = "bolero-libfuzzer" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98" -dependencies = [ - "bolero-engine", - "cc", -] - -[[package]] -name = "bounded-integer" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a6932c88f1d2c29533a3b8a5f5a2f84cc19c3339b431677c3160c5c2e6ca85" - -[[package]] -name = "brotli" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "buffer-redux" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2886ea01509598caac116942abd33ab5a88fa32acdf7e4abfa0fc489ca520c9" -dependencies = [ - "memchr", - "safemem", -] - -[[package]] -name = "bumpalo" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" - -[[package]] -name = "byte_string" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" - -[[package]] -name = "bytemuck" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" -dependencies = [ - "serde", -] - -[[package]] -name = "camellia" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" -dependencies = [ - "byteorder", - "cipher", -] - -[[package]] -name = "cast5" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" -dependencies = [ - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb-mode" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" -dependencies = [ - "cipher", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "charset" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" -dependencies = [ - "base64 0.13.1", - "encoding_rs", -] - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets 0.52.6", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "cmac" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" -dependencies = [ - "cipher", - "dbl", - "digest", -] - -[[package]] -name = "cobs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" - -[[package]] -name = "const_format" -version = "0.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - -[[package]] -name = "cordyceps" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3" -dependencies = [ - "loom", - "tracing", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc24" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "crypto_box" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" -dependencies = [ - "aead", - "chacha20", - "crypto_secretbox", - "curve25519-dalek", - "salsa20", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto_secretbox" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" -dependencies = [ - "aead", - "chacha20", - "cipher", - "generic-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto 0.2.6", - "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.90", -] - -[[package]] -name = "cxx" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.107", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.90", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "dashmap" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" -dependencies = [ - "cfg-if", - "hashbrown 0.12.3", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" - -[[package]] -name = "dbl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" -dependencies = [ - "generic-array", -] - -[[package]] -name = "deltachat" -version = "1.151.5" -dependencies = [ - "anyhow", - "async-broadcast", - "async-channel 2.3.1", - "async-imap", - "async-native-tls", - "async-smtp", - "async_zip", - "base64 0.22.1", - "brotli", - "bytes", - "chrono", - "deltachat-contact-tools", - "deltachat-time", - "deltachat_derive", - "email", - "encoded-words", - "escaper", - "fast-socks5", - "fd-lock", - "format-flowed", - "futures", - "futures-lite 2.5.0", - "hex", - "hickory-resolver", - "http-body-util", - "humansize", - "hyper", - "hyper-util", - "image", - "iroh-gossip", - "iroh-net", - "kamadak-exif", - "lettre_email", - "libc", - "mailparse 0.15.0", - "mime", - "num-derive", - "num-traits", - "num_cpus", - "once_cell", - "parking_lot", - "percent-encoding", - "pgp", - "pin-project", - "qrcodegen", - "quick-xml", - "quoted_printable 0.5.0", - "rand 0.8.5", - "ratelimit", - "regex", - "rusqlite", - "rust-hsluv", - "rustls", - "rustls-pki-types", - "sanitize-filename", - "serde", - "serde_json", - "serde_urlencoded", - "sha-1", - "sha2", - "shadowsocks", - "smallvec", - "strum", - "strum_macros", - "tagger", - "textwrap", - "thiserror 1.0.58", - "tokio", - "tokio-io-timeout", - "tokio-rustls", - "tokio-stream", - "tokio-tar", - "tokio-util", - "toml", - "url", - "uuid 1.2.2", - "webpki-roots", -] - -[[package]] -name = "deltachat-contact-tools" -version = "0.0.0" -dependencies = [ - "anyhow", - "chrono", - "once_cell", - "regex", - "rusqlite", -] - -[[package]] -name = "deltachat-fuzz" -version = "0.0.0" -dependencies = [ - "bolero", - "deltachat", - "format-flowed", - "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.90", -] - -[[package]] -name = "der" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b10af9f9f9f2134a42d3f8aa74658660f2e0234b0eb81bd171df8aa32779ed" -dependencies = [ - "const-oid", - "der_derive", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_builder" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" -dependencies = [ - "derive_builder_core", - "syn 2.0.90", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", - "unicode-xid", -] - -[[package]] -name = "des" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" -dependencies = [ - "cipher", -] - -[[package]] -name = "diatomic-waker" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "dlopen2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" -dependencies = [ - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "document-features" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" -dependencies = [ - "litrs", -] - -[[package]] -name = "dsa" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" -dependencies = [ - "digest", - "num-bigint-dig", - "num-traits", - "pkcs8", - "rfc6979", - "sha2", - "signature", - "zeroize", -] - -[[package]] -name = "dtoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" - -[[package]] -name = "duct" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" -dependencies = [ - "libc", - "once_cell", - "os_pipe", - "shared_child", -] - -[[package]] -name = "dyn-clone" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" - -[[package]] -name = "eax" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" -dependencies = [ - "aead", - "cipher", - "cmac", - "ctr", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48e5d537b8a30c0b023116d981b16334be1485af7ca68db3a2b7024cbc957fd" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", -] - -[[package]] -name = "ed25519" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" -dependencies = [ - "pkcs8", - "serde", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", -] - -[[package]] -name = "either" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "email" -version = "0.0.21" -source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e" -dependencies = [ - "base64 0.11.0", - "chrono", - "encoded-words", - "encoding", - "lazy_static", - "rand 0.7.3", - "time 0.1.45", - "version_check", -] - -[[package]] -name = "encoded-words" -version = "0.2.0" -source = "git+https://github.com/async-email/encoded-words?branch=master#d55366b36f96e383f39c432aedce42ee8b43f796" -dependencies = [ - "base64 0.12.3", - "charset", - "encoding_rs", - "hex", - "lazy_static", - "regex", - "thiserror 1.0.58", -] - -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", -] - -[[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-korean" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" -dependencies = [ - "encoding_index_tests", -] - -[[package]] -name = "encoding_index_tests" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" - -[[package]] -name = "encoding_rs" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "entities" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" - -[[package]] -name = "enum-as-inner" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "enumflags2" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - -[[package]] -name = "erased_set" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5aa24577083f8190ad401e376b55887c7cd9083ae95d83ceec5d28ea78125" - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "escaper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53eb97b7349ba1bdb31839eceafe9aaae8f1d8d944dc589b67fb0b26e1c1666" -dependencies = [ - "entities", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.1", - "pin-project-lite", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fast-socks5" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d449e348301d5fb9b0e5781510d8235ffe3bbac3286bd305462736a9e7043039" -dependencies = [ - "anyhow", - "async-trait", - "log", - "thiserror 1.0.58", - "tokio", - "tokio-stream", -] - -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - -[[package]] -name = "fastrand" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "fd-lock" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93f7a0db71c99f68398f80653ed05afb0b00e062e1a20c7ff849c4edfabbbcc" -dependencies = [ - "cfg-if", - "rustix 0.38.14", - "windows-sys 0.52.0", -] - -[[package]] -name = "ff" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "fiat-crypto" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" - -[[package]] -name = "filetime" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.42.0", -] - -[[package]] -name = "flate2" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "format-flowed" -version = "1.0.0" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-buffered" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34acda8ae8b63fbe0b2195c998b180cff89a8212fb2622a78b572a9f1c6f7684" -dependencies = [ - "cordyceps", - "diatomic-waker", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-concurrency" -version = "7.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b14ac911e85d57c5ea6eef76d7b4d4a3177ecd15f4bea2e61927e9e3823e19f" -dependencies = [ - "bitvec", - "futures-buffered", - "futures-core", - "futures-lite 1.13.0", - "pin-project", - "slab", - "smallvec", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.8.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" -dependencies = [ - "fastrand 2.0.1", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "genawaiter" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" -dependencies = [ - "futures-core", - "genawaiter-macro", - "genawaiter-proc-macro", - "proc-macro-hack", -] - -[[package]] -name = "genawaiter-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" - -[[package]] -name = "genawaiter-proc-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" -dependencies = [ - "proc-macro-error", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gif" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "governor" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" -dependencies = [ - "cfg-if", - "dashmap", - "futures", - "futures-timer", - "no-std-compat", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.8.5", - "smallvec", - "spinning_top", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 1.1.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] - -[[package]] -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" -dependencies = [ - "hashbrown 0.14.3", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hickory-proto" -version = "0.25.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8270a1857fb962b9914aafd46a89a187a4e63d0eb4190c327e7c7b8256a2d055" -dependencies = [ - "async-recursion", - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.8.5", - "thiserror 1.0.58", - "time 0.3.36", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c110355b5703070d9e29c344d79818a7cde3de9c27fc35750defea6074b0ad" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "lru-cache", - "once_cell", - "parking_lot", - "rand 0.8.5", - "resolv-conf", - "smallvec", - "thiserror 1.0.58", - "tokio", - "tracing", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "hmac-sha1" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b05da5b9e5d4720bfb691eebb2b9d42da3570745da71eac8a1f5bb7e59aab88" -dependencies = [ - "hmac", - "sha1", -] - -[[package]] -name = "hmac-sha256" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" - -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - -[[package]] -name = "hostname-validator" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humansize" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" -dependencies = [ - "libm", -] - -[[package]] -name = "hyper" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http 1.1.0", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" -dependencies = [ - "futures-util", - "http 1.1.0", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body", - "hyper", - "pin-project-lite", - "socket2 0.5.6", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "idea" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" -dependencies = [ - "cipher", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "igd-next" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http 1.1.0", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.8.5", - "tokio", - "url", - "xmltree", -] - -[[package]] -name = "image" -version = "0.25.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "gif", - "image-webp", - "num-traits", - "png", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" -dependencies = [ - "byteorder-lite", - "quick-error 2.0.1", -] - -[[package]] -name = "imap-proto" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e70cd66882c8cb1c9802096ba75212822153c51478dc61621e1a22f6c92361" -dependencies = [ - "nom", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown 0.14.3", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" -dependencies = [ - "libc", - "windows-sys 0.42.0", -] - -[[package]] -name = "ipconfig" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" -dependencies = [ - "socket2 0.4.7", - "widestring", - "winapi", - "winreg 0.10.1", -] - -[[package]] -name = "ipnet" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" - -[[package]] -name = "iroh-base" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c21fd8eb71f166a172a9779c2244db992218e9a9bd929b9df6fc355d2b630c9" -dependencies = [ - "aead", - "anyhow", - "crypto_box", - "data-encoding", - "derive_more", - "ed25519-dalek", - "getrandom 0.2.11", - "hex", - "iroh-blake3", - "once_cell", - "postcard", - "rand 0.8.5", - "rand_core 0.6.4", - "serde", - "ssh-key", - "thiserror 1.0.58", - "ttl_cache", - "url", - "zeroize", -] - -[[package]] -name = "iroh-blake3" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbba31f40a650f58fa28dd585a8ca76d8ae3ba63aacab4c8269004a0c803930" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "iroh-gossip" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c078057037f0e741c5ef285c67fd9cfdb928163dd046fb547089898bdb02990e" -dependencies = [ - "anyhow", - "async-channel 2.3.1", - "bytes", - "derive_more", - "ed25519-dalek", - "futures-concurrency", - "futures-lite 2.5.0", - "futures-util", - "indexmap", - "iroh-base", - "iroh-blake3", - "iroh-metrics", - "iroh-net", - "iroh-router", - "postcard", - "rand 0.8.5", - "rand_core 0.6.4", - "serde", - "serde-error", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "iroh-metrics" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d40f2ee3997489d47403d204a06514ed65373d224b5b43a8ea133f543e5db1" -dependencies = [ - "anyhow", - "erased_set", - "http-body-util", - "hyper", - "hyper-util", - "once_cell", - "prometheus-client", - "reqwest", - "serde", - "struct_iterable", - "time 0.3.36", - "tokio", - "tracing", -] - -[[package]] -name = "iroh-net" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40e1f1f9029e198c6d05bd232d3239814b0a66ac4668978729b709aeb6a44e2" -dependencies = [ - "anyhow", - "backoff", - "base64 0.22.1", - "bytes", - "der", - "derive_more", - "duct", - "futures-buffered", - "futures-concurrency", - "futures-lite 2.5.0", - "futures-sink", - "futures-util", - "genawaiter", - "governor", - "hex", - "hickory-proto", - "hickory-resolver", - "hostname", - "http 1.1.0", - "http-body-util", - "hyper", - "hyper-util", - "igd-next", - "iroh-base", - "iroh-metrics", - "iroh-quinn", - "iroh-quinn-proto", - "iroh-quinn-udp", - "libc", - "netdev", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "netwatch", - "num_enum", - "once_cell", - "parking_lot", - "pin-project", - "pkarr", - "portmapper", - "postcard", - "rand 0.8.5", - "rcgen", - "reqwest", - "ring", - "rtnetlink", - "rustls", - "rustls-webpki", - "serde", - "smallvec", - "socket2 0.5.6", - "strum", - "stun-rs", - "surge-ping", - "thiserror 1.0.58", - "time 0.3.36", - "tokio", - "tokio-rustls", - "tokio-stream", - "tokio-tungstenite", - "tokio-tungstenite-wasm", - "tokio-util", - "tracing", - "tungstenite", - "url", - "watchable", - "webpki-roots", - "windows 0.51.1", - "wmi", - "x509-parser", - "z32", -] - -[[package]] -name = "iroh-quinn" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ba75a5c57cff299d2d7ca1ddee053f66339d1756bd79ec637bcad5aa61100e" -dependencies = [ - "bytes", - "iroh-quinn-proto", - "iroh-quinn-udp", - "pin-project-lite", - "rustc-hash 2.0.0", - "rustls", - "socket2 0.5.6", - "thiserror 1.0.58", - "tokio", - "tracing", -] - -[[package]] -name = "iroh-quinn-proto" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c869ba52683d3d067c83ab4c00a2fda18eaf13b1434d4c1352f428674d4a5d" -dependencies = [ - "bytes", - "rand 0.8.5", - "ring", - "rustc-hash 2.0.0", - "rustls", - "rustls-platform-verifier", - "slab", - "thiserror 1.0.58", - "tinyvec", - "tracing", -] - -[[package]] -name = "iroh-quinn-udp" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfc0abc2fdf8cf18a6c72893b7cbebeac2274a3b1306c1760c48c0e10ac5e0" -dependencies = [ - "libc", - "once_cell", - "socket2 0.5.6", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "iroh-router" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd18ec6325dd3f01625f12c01acff50a4374ee1ab708e7b2078885fd63ad30" -dependencies = [ - "anyhow", - "futures-buffered", - "futures-lite 2.5.0", - "futures-util", - "iroh-net", - "tokio", - "tokio-util", - "tracing", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror 1.0.58", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -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", - "elliptic-curve", - "once_cell", - "sha2", - "signature", -] - -[[package]] -name = "kamadak-exif" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" -dependencies = [ - "mutate_once", -] - -[[package]] -name = "keccak" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] - -[[package]] -name = "lettre" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" -dependencies = [ - "fast_chemail", - "log", -] - -[[package]] -name = "lettre_email" -version = "0.9.2" -source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" -dependencies = [ - "base64 0.11.0", - "email", - "lazy_static", - "lettre", - "mime", - "regex", - "time 0.1.45", - "uuid 0.8.2", -] - -[[package]] -name = "libc" -version = "0.2.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" - -[[package]] -name = "libm" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "openssl-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" - -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" - -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "lru_time_cache" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" - -[[package]] -name = "mailparse" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" -dependencies = [ - "charset", - "data-encoding", - "quoted_printable 0.4.6", -] - -[[package]] -name = "mailparse" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da03d5980411a724e8aaf7b61a7b5e386ec55a7fb49ee3d0ff79efc7e5e7c7e" -dependencies = [ - "charset", - "data-encoding", - "quoted_printable 0.5.0", -] - -[[package]] -name = "mainline" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" -dependencies = [ - "bytes", - "crc", - "ed25519-dalek", - "flume", - "lru", - "rand 0.8.5", - "serde", - "serde_bencode", - "serde_bytes", - "sha1_smol", - "thiserror 1.0.58", - "tracing", -] - -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "md-5" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" -dependencies = [ - "digest", -] - -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "memalloc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" - -[[package]] -name = "memchr" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "mutate_once" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.11", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "netdev" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7516ad2c46cc25da098ed7d6b9a0cbe9e1fbffbd04b1596148b95f2841179c83" -dependencies = [ - "dlopen2", - "libc", - "memalloc", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "once_cell", - "system-configuration", - "windows-sys 0.52.0", -] - -[[package]] -name = "netlink-packet-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" -dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "byteorder", - "libc", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.58", -] - -[[package]] -name = "netlink-proto" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 1.0.58", - "tokio", -] - -[[package]] -name = "netlink-sys" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" -dependencies = [ - "bytes", - "futures", - "libc", - "log", - "tokio", -] - -[[package]] -name = "netwatch" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a639d52c0996ac640e2a7052a5265c8f71efdbdadc83188435ffc358b7ca931" -dependencies = [ - "anyhow", - "bytes", - "derive_more", - "futures-lite 2.5.0", - "futures-sink", - "futures-util", - "libc", - "netdev", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "once_cell", - "rtnetlink", - "serde", - "socket2 0.5.6", - "thiserror 1.0.58", - "time 0.3.36", - "tokio", - "tracing", - "windows 0.51.1", - "wmi", -] - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" - -[[package]] -name = "nom" -version = "7.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "serde", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "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.90", -] - -[[package]] -name = "object" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "ocb3" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" -dependencies = [ - "aead", - "cipher", - "ctr", - "subtle", -] - -[[package]] -name = "oid-registry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" -dependencies = [ - "asn1-rs", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-src" -version = "300.3.1+3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "os_pipe" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "p256" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7304b8213f8597952b2f4c3d7f09947d46bb677801df8f287ffd2c4e26f9da" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p521" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" -dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", - "rand_core 0.6.4", - "sha2", -] - -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.42.0", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "pem" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pest" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" -dependencies = [ - "memchr", - "thiserror 1.0.58", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "pest_meta" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "pgp" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9" -dependencies = [ - "aes", - "aes-gcm", - "aes-kw", - "argon2", - "base64 0.22.1", - "bitfield", - "block-padding", - "blowfish", - "bstr", - "buffer-redux", - "byteorder", - "camellia", - "cast5", - "cfb-mode", - "chrono", - "cipher", - "const-oid", - "crc24", - "curve25519-dalek", - "derive_builder", - "derive_more", - "des", - "digest", - "dsa", - "eax", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", - "flate2", - "generic-array", - "hex", - "hkdf", - "idea", - "iter-read", - "k256", - "log", - "md-5", - "nom", - "num-bigint-dig", - "num-traits", - "num_enum", - "ocb3", - "p256", - "p384", - "p521", - "rand 0.8.5", - "ripemd", - "rsa", - "sha1", - "sha1-checked", - "sha2", - "sha3", - "signature", - "smallvec", - "thiserror 1.0.58", - "twofish", - "x25519-dalek", - "x448", - "zeroize", -] - -[[package]] -name = "pin-project" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkarr" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8" -dependencies = [ - "bytes", - "document-features", - "dyn-clone", - "ed25519-dalek", - "flume", - "futures", - "js-sys", - "lru", - "mainline", - "self_cell", - "simple-dns", - "thiserror 1.0.58", - "tracing", - "ureq", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "z32", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "pnet_base" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" -dependencies = [ - "no-std-net", -] - -[[package]] -name = "pnet_macros" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.90", -] - -[[package]] -name = "pnet_macros_support" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - -[[package]] -name = "png" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" - -[[package]] -name = "portmapper" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d60045fdcfe8ff6b781cf1027fdbb08ed319d93aff7da4bedc018e3bc92226" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bytes", - "derive_more", - "futures-lite 2.5.0", - "futures-util", - "igd-next", - "iroh-metrics", - "libc", - "netwatch", - "num_enum", - "rand 0.8.5", - "serde", - "smallvec", - "socket2 0.5.6", - "thiserror 1.0.58", - "time 0.3.36", - "tokio", - "tokio-util", - "tracing", - "url", -] - -[[package]] -name = "postcard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" -dependencies = [ - "cobs", - "const_format", - "postcard-derive", - "serde", -] - -[[package]] -name = "postcard-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4b01218787dd4420daf63875163a787a78294ad48a24e9f6fa8c6507759a79" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "precis-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73e9dd26361c32e7cd13d1032bb01c4e26a23287274e8a4e2f228cf2c9ff77b" -dependencies = [ - "precis-tools", - "ucd-parse", - "unicode-normalization", -] - -[[package]] -name = "precis-profiles" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde4bd6624c60cb0abe2bea1dbdbb9085f629a853861e64df4abb099f8076ad4" -dependencies = [ - "lazy_static", - "precis-core", - "precis-tools", - "unicode-normalization", -] - -[[package]] -name = "precis-tools" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07ecadec70b0f560f09abf815ae0ee1a940d38d2354c938ba7229ac7c9f5f52" -dependencies = [ - "lazy_static", - "regex", - "ucd-parse", -] - -[[package]] -name = "pretty-hex" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[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 = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.107", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", - "syn-mid", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prometheus-client" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" -dependencies = [ - "dtoa", - "itoa", - "parking_lot", - "prometheus-client-derive-encode", -] - -[[package]] -name = "prometheus-client-derive-encode" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "qrcodegen" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" - -[[package]] -name = "quanta" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 1.1.0", - "rustls", - "thiserror 1.0.58", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom 0.2.11", - "rand 0.8.5", - "ring", - "rustc-hash 2.0.0", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.6", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a78e6f726d84fcf960409f509ae354a32648f090c8d32a2ea8b1a1bc3bab14" -dependencies = [ - "libc", - "once_cell", - "socket2 0.5.6", - "windows-sys 0.52.0", -] - -[[package]] -name = "quote" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quoted-string-parser" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc75379cdb451d001f1cb667a9f74e8b355e9df84cc5193513cbe62b96fc5e9" -dependencies = [ - "pest", - "pest_derive", -] - -[[package]] -name = "quoted_printable" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" - -[[package]] -name = "quoted_printable" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.11", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "ratelimit" -version = "1.0.0" - -[[package]] -name = "raw-cpuid" -version = "11.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" -dependencies = [ - "bitflags 2.6.0", -] - -[[package]] -name = "rcgen" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" -dependencies = [ - "pem", - "ring", - "time 0.3.36", - "yasna", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "regex" -version = "1.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.28", -] - -[[package]] -name = "regex-automata" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "reqwest" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg 0.52.0", -] - -[[package]] -name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error 1.2.3", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" -dependencies = [ - "cc", - "getrandom 0.2.11", - "libc", - "spin 0.9.8", - "untrusted", - "windows-sys 0.48.0", -] - -[[package]] -name = "ring-compat" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" -dependencies = [ - "aead", - "digest", - "ecdsa", - "ed25519", - "generic-array", - "p256", - "p384", - "pkcs8", - "rand_core 0.6.4", - "ring", - "signature", -] - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest", -] - -[[package]] -name = "rsa" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rtnetlink" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" -dependencies = [ - "futures", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-packet-utils", - "netlink-proto", - "netlink-sys", - "nix", - "thiserror 1.0.58", - "tokio", -] - -[[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags 2.6.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rust-hsluv" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b" - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" -dependencies = [ - "bitflags 1.3.2", - "errno 0.2.8", - "io-lifetimes", - "libc", - "linux-raw-sys 0.1.4", - "windows-sys 0.42.0", -] - -[[package]] -name = "rustix" -version = "0.38.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" -dependencies = [ - "bitflags 2.6.0", - "errno 0.3.3", - "libc", - "linux-raw-sys 0.4.7", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustls" -version = "0.23.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" -dependencies = [ - "base64 0.21.7", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" -dependencies = [ - "web-time", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-roots", - "winapi", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" - -[[package]] -name = "ryu" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "sanitize-filename" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "schannel" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" -dependencies = [ - "lazy_static", - "windows-sys 0.36.1", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" - -[[package]] -name = "sec1" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-foundation-sys", - "libc", - "num-bigint", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "self_cell" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "sendfd" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798" -dependencies = [ - "libc", - "tokio", -] - -[[package]] -name = "serde" -version = "1.0.204" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-error" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342110fb7a5d801060c885da03bf91bfa7c7ca936deafcc64bb6706375605d47" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_bencode" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" -dependencies = [ - "serde", - "serde_bytes", -] - -[[package]] -name = "serde_bytes" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.204" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "serde_json" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest", - "sha1", - "zeroize", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha3" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "shadowsocks" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ecb3780dfbc654de9383758015b9bb95c6e32fecace36ebded09d67e854d130" -dependencies = [ - "aes", - "async-trait", - "base64 0.22.1", - "blake3", - "byte_string", - "bytes", - "cfg-if", - "futures", - "libc", - "log", - "lru_time_cache", - "once_cell", - "percent-encoding", - "pin-project", - "rand 0.8.5", - "sendfd", - "serde", - "serde_json", - "serde_urlencoded", - "shadowsocks-crypto", - "socket2 0.5.6", - "spin 0.9.8", - "thiserror 1.0.58", - "tokio", - "tokio-tfo", - "url", - "windows-sys 0.59.0", -] - -[[package]] -name = "shadowsocks-crypto" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc77ecb3a97509d22751b76665894fcffad2d10df8758f4e3f20c92ccde6bf4f" -dependencies = [ - "aes", - "aes-gcm", - "blake3", - "bytes", - "cfg-if", - "chacha20poly1305", - "hkdf", - "md-5", - "rand 0.8.5", - "ring-compat", - "sha1", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shared_child" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simple-dns" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" -dependencies = [ - "bitflags 2.6.0", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "smawk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" - -[[package]] -name = "socket2" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "ssh-cipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" -dependencies = [ - "cipher", - "ssh-encoding", -] - -[[package]] -name = "ssh-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" -dependencies = [ - "base64ct", - "pem-rfc7468", - "sha2", -] - -[[package]] -name = "ssh-key" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0a17fec6ea344bfa1cda3aed2f0696fddc6295cfcc8c454a3bf58b8ffaabeb" -dependencies = [ - "ed25519-dalek", - "p256", - "p384", - "rand_core 0.6.4", - "rsa", - "sec1", - "sha2", - "signature", - "ssh-cipher", - "ssh-encoding", - "subtle", - "zeroize", -] - -[[package]] -name = "stop-token" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" -dependencies = [ - "async-channel 1.8.0", - "cfg-if", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "struct_iterable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849a064c6470a650b72e41fa6c057879b68f804d113af92900f27574828e7712" -dependencies = [ - "struct_iterable_derive", - "struct_iterable_internal", -] - -[[package]] -name = "struct_iterable_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb939ce88a43ea4e9d012f2f6b4cc789deb2db9d47bad697952a85d6978662c" -dependencies = [ - "erased-serde", - "proc-macro2", - "quote", - "struct_iterable_internal", - "syn 2.0.90", -] - -[[package]] -name = "struct_iterable_internal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9426b2a0c03e6cc2ea8dbc0168dbbf943f88755e409fb91bcb8f6a268305f4a" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.90", -] - -[[package]] -name = "stun-rs" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adebf9fb8fba5c39ee34092b0383f247e4d1255b98fcffec94b4b797b85b677" -dependencies = [ - "base64 0.22.1", - "bounded-integer", - "byteorder", - "crc", - "enumflags2", - "fallible-iterator", - "hmac-sha1", - "hmac-sha256", - "hostname-validator", - "lazy_static", - "md5", - "paste", - "precis-core", - "precis-profiles", - "quoted-string-parser", - "rand 0.8.5", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "surge-ping" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbf95ce4c7c5b311d2ce3f088af2b93edef0f09727fa50fbe03c7a979afce77" -dependencies = [ - "hex", - "parking_lot", - "pnet_packet", - "rand 0.8.5", - "socket2 0.5.6", - "thiserror 1.0.58", - "tokio", - "tracing", -] - -[[package]] -name = "syn" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-mid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", - "unicode-xid", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "system-configuration" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagger" -version = "4.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" -dependencies = [ - "cfg-if", - "fastrand 1.8.0", - "redox_syscall", - "rustix 0.36.7", - "windows-sys 0.42.0", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" -dependencies = [ - "thiserror-impl 1.0.58", -] - -[[package]] -name = "thiserror" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" -dependencies = [ - "thiserror-impl 2.0.6", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.41.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.5.6", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tar" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" -dependencies = [ - "filetime", - "futures-core", - "libc", - "redox_syscall", - "tokio", - "tokio-stream", - "xattr", -] - -[[package]] -name = "tokio-tfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb4382c6371e29365853d2b71e915d5398df46312a2158097d8bb3f54d0f1b4" -dependencies = [ - "cfg-if", - "futures", - "libc", - "log", - "once_cell", - "pin-project", - "socket2 0.5.6", - "tokio", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-tungstenite-wasm" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e57a65894797a018b28345fa298a00c450a574aa9671e50b18218a6292a55ac" -dependencies = [ - "futures-channel", - "futures-util", - "http 1.1.0", - "httparse", - "js-sys", - "thiserror 1.0.58", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "futures-util", - "hashbrown 0.14.3", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "ttl_cache" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4189890526f0168710b6ee65ceaedf1460c48a14318ceec933cb26baa492096a" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.1.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.58", - "url", - "utf-8", -] - -[[package]] -name = "twofish" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" -dependencies = [ - "cipher", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "ucd-parse" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06ff81122fcbf4df4c1660b15f7e3336058e7aec14437c9f85c6b31a0f279b9" -dependencies = [ - "regex-lite", -] - -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" - -[[package]] -name = "unicode-linebreak" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" -dependencies = [ - "hashbrown 0.12.3", - "regex", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" -dependencies = [ - "base64 0.22.1", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - -[[package]] -name = "url" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.11", -] - -[[package]] -name = "uuid" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" -dependencies = [ - "getrandom 0.2.11", - "serde", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.90", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "watchable" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b42a2f611916b5965120a9cde2b60f2db4454826dd9ad5e6f47c24a5b3b259" -dependencies = [ - "event-listener 4.0.3", - "futures-util", - "parking_lot", - "thiserror 1.0.58", -] - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "weezl" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" - -[[package]] -name = "widestring" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" -dependencies = [ - "windows-core 0.51.1", - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-implement", - "windows-interface", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "windows-interface" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wmi" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a" -dependencies = [ - "chrono", - "futures", - "log", - "serde", - "thiserror 1.0.58", - "windows 0.52.0", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 1.0.58", - "time 0.3.36", -] - -[[package]] -name = "xattr" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" -dependencies = [ - "libc", -] - -[[package]] -name = "xml-rs" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time 0.3.36", -] - -[[package]] -name = "z32" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" - -[[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.90", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", - "synstructure 0.12.6", -] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-jpeg" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" -dependencies = [ - "zune-core", -] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a3eba6256b..2379025316 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -3,28 +3,21 @@ name = "deltachat-fuzz" version = "0.0.0" publish = false edition = "2021" +license = "MPL-2.0" [dev-dependencies] bolero = "0.8" [dependencies] -mailparse = "0.13" +mailparse = { workspace = true } deltachat = { path = ".." } format-flowed = { path = "../format-flowed" } -[workspace] -members = ["."] - [[test]] name = "fuzz_dateparse" path = "fuzz_targets/fuzz_dateparse.rs" harness = false -[[test]] -name = "fuzz_simplify" -path = "fuzz_targets/fuzz_simplify.rs" -harness = false - [[test]] name = "fuzz_mailparse" path = "fuzz_targets/fuzz_mailparse.rs" diff --git a/fuzz/fuzz_targets/fuzz_format_flowed.rs b/fuzz/fuzz_targets/fuzz_format_flowed.rs index 8f779a4680..e89d8dd749 100644 --- a/fuzz/fuzz_targets/fuzz_format_flowed.rs +++ b/fuzz/fuzz_targets/fuzz_format_flowed.rs @@ -9,7 +9,7 @@ fn round_trip(input: &str) -> String { fn main() { check!().for_each(|data: &[u8]| { - if let Ok(input) = std::str::from_utf8(data.into()) { + if let Ok(input) = std::str::from_utf8(data) { let input = input.trim().to_string(); // Only consider inputs that are the result of unformatting format=flowed text. diff --git a/fuzz/fuzz_targets/fuzz_simplify.rs b/fuzz/fuzz_targets/fuzz_simplify.rs deleted file mode 100644 index cd0a22352d..0000000000 --- a/fuzz/fuzz_targets/fuzz_simplify.rs +++ /dev/null @@ -1,13 +0,0 @@ -use bolero::check; - -use deltachat::fuzzing::simplify; - -fn main() { - check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) { - Ok(input) => { - simplify(input.clone(), true); - simplify(input, false); - } - Err(_err) => {} - }); -} diff --git a/node/.prettierrc.yml b/node/.prettierrc.yml deleted file mode 100644 index 5ca635d596..0000000000 --- a/node/.prettierrc.yml +++ /dev/null @@ -1,6 +0,0 @@ -# .prettierrc -trailingComma: es5 -tabWidth: 2 -semi: false -singleQuote: true -jsxSingleQuote: true diff --git a/node/CONTRIBUTORS.md b/node/CONTRIBUTORS.md deleted file mode 100644 index 1c81d4add7..0000000000 --- a/node/CONTRIBUTORS.md +++ /dev/null @@ -1,21 +0,0 @@ -# Contributors - -| Name | GitHub | -| :-------------------- | :----------------------------------------------- | -| **Lars-Magnus Skog** | | -| **jikstra** | | -| **Simon Laux** | [**@Simon-Laux**](https://github.com/Simon-Laux) | -| **Jikstra** | [**@Jikstra**](https://github.com/Jikstra) | -| **Nico de Haen** | | -| **B. Petersen** | | -| **Karissa McKelvey** | [**@karissa**](https://github.com/karissa) | -| **developer** | | -| **Alexander Krotov** | | -| **Floris Bruynooghe** | | -| **lefherz** | | -| **Pablo** | [**@pabzm**](https://github.com/pabzm) | -| **pabzm** | | -| **holger krekel** | | -| **Robert Schütz** | | -| **bb** | | -| **Charles Paul** | | diff --git a/node/LICENSE b/node/LICENSE deleted file mode 100644 index 94a9ed024d..0000000000 --- a/node/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/node/README.md b/node/README.md deleted file mode 100644 index 93a0beb76b..0000000000 --- a/node/README.md +++ /dev/null @@ -1,260 +0,0 @@ -# deltachat-node - -> node.js bindings for [`deltachat-core-rust`](..) - -[![npm](https://img.shields.io/npm/v/deltachat-node.svg)](https://www.npmjs.com/package/deltachat-node) -![Node version](https://img.shields.io/node/v/deltachat-node.svg) -[![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) - -`deltachat-node` primarily aims to offer two things: - -- A high level JavaScript api with syntactic sugar -- A low level c binding api around [`deltachat-core-rust`](..) - -This code used to live at [`deltachat-node`](https://github.com/deltachat/deltachat-node) - -## Table of Contents - -
Click to expand - -- [Install](#install) -- [Dependencies](#dependencies) -- [Build from source](#build-from-source) -- [Usage](#usage) -- [Developing](#developing) -- [License](#license) - -
- -## Install - -By default the installation will try to use the bundled prebuilds in the -npm package. If this fails it falls back to compile `../deltachat-core-rust` from -this repository, using `scripts/rebuild-core.js`. - -To install from npm use: - -``` -npm install deltachat-node -``` - -## Dependencies - -- Nodejs >= `v18.0.0` -- rustup (optional if you can't use the prebuilds) - -> On Windows, you may need to also install **Perl** to be able to compile deltachat-core. - -## Build from source - -If you want to build from source, make sure that you have `rustup` installed. -You can either use `npm install deltachat-node --build-from-source` to force -building from source or clone this repository and follow this steps: - -1. `git clone https://github.com/deltachat/deltachat-core-rust.git` -2. `cd deltachat-core-rust` -3. `npm i` -4. `npm run build` - -> Our `package.json` file is located in the root directory of this repository, -> not inside this folder. (We need this in order to include the rust source -> code in the npm package.) - -### Use a git branch in deltachat-desktop - -You can directly install a core branch, but make sure: -- that you have typescript in your project dependencies, as it is likely required -- you know that there are **no prebuilds** and so core is built during installation which is why it takes so long - -``` -npm install https://github.com/deltachat/deltachat-core-rust.git#branch -``` - -If you want prebuilds for a branch that has a core pr, you might find an npm tar.gz package for that branch at . -The github ci also posts a link to it in the checks for each pr. - -### Use build-from-source in deltachat-desktop - -If you want to use the manually built node bindings in the desktop client (for -example), you can follow these instructions: - -First clone the -[deltachat-desktop](https://github.com/deltachat/deltachat-desktop) repository, -e.g. with `git clone https://github.com/deltachat/deltachat-desktop`. - -Then you need to make sure that this directory is referenced correctly in -deltachat-desktop's package.json. You need to change -`deltachat-desktop/package.json` like this: - -``` -diff --git i/package.json w/package.json -index 45893894..5154512c 100644 ---- i/package.json -+++ w/package.json -@@ -83,7 +83,7 @@ - "application-config": "^1.0.1", - "classnames": "^2.3.1", - "debounce": "^1.2.0", -- "deltachat-node": "1.79.3", -+ "deltachat-node": "file:../deltachat-core-rust/", - "emoji-js-clean": "^4.0.0", - "emoji-mart": "^3.0.1", - "emoji-regex": "^9.2.2", -``` - -Then, in the `deltachat-desktop` repository, run: - -1. `npm i` -2. `npm run build` -3. And `npm run start` to start the newly built client. - -### Workaround to build for x86_64 on Apple's M1 - -deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds: - -``` -$ fnm install 19 --arch x64 -$ fnm use 19 -$ node -p process.arch -# result should be x64 -$ rustup target add x86_64-apple-darwin -$ git apply patches/m1_build_use_x86_64.patch -$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build -$ npm run test -``` - -(when using [fnm](https://github.com/Schniz/fnm) instead of nvm, you can select the architecture) -If your node and electron are already build for arm64 you can also try building for arm: - -``` -$ fnm install 18 --arch arm64 -$ fnm use 18 -$ node -p process.arch -# result should be arm64 -$ npm_config_arch=arm64 npm run build -$ npm run test -``` - -## Usage - -```js -const { Context } = require('deltachat-node') - -const opts = { - addr: '[email]', - mail_pw: '[password]', -} - -const contact = '[email]' - -async function main() { - const dc = Context.open('./') - dc.on('ALL', console.log.bind(null, 'core |')) - - try { - await dc.configure(opts) - } catch (err) { - console.error('Failed to configure because of: ', err) - dc.unref() - return - } - - dc.startIO() - console.log('fully configured') - - const contactId = dc.createContact('Test', contact) - const chatId = dc.createChatByContactId(contactId) - dc.sendMessage(chatId, 'Hi!') - - console.log('sent message') - - dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => { - console.log('Message sent, shutting down...') - dc.stopIO() - console.log('stopped io') - dc.unref() - }) -} - -main() -``` -this example can also be found in the examples folder [examples/send_message.js](./examples/send_message.js) - -### Generating Docs - -We are currently migrating to automatically generated documentation. -You can find the old documentation at [old_docs](./old_docs). - -to generate the documentation, run: - -``` -npx typedoc -``` - -The resulting documentation can be found in the `docs/` folder. -An online version can be found under [js.delta.chat](https://js.delta.chat). - -## Developing - -### Tests and Coverage - -Running `npm test` ends with showing a code coverage report, which is produced by [`nyc`](https://github.com/istanbuljs/nyc#readme). - -![test output](images/tests.png) - -The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine. - -To run the integration tests you need to set the `CHATMAIL_DOMAIN` environment variables. E.g.: - -``` -$ export CHATMAIL_DOMAIN=chat.example.org -$ npm run test -``` - -### Scripts - -We have the following scripts for building, testing and coverage: - -- `npm run coverage` Creates a coverage report and passes it to `coveralls`. Only done by `Travis`. -- `npm run coverage-html-report` Generates a html report from the coverage data and opens it in a browser on the local machine. -- `npm run generate-constants` Generates `constants.js` and `events.js` based on the `deltachat-core-rust/deltachat-ffi/deltachat.h` header file. -- `npm install` After dependencies are installed, runs `node-gyp-build` to see if the native code needs to be rebuilt. -- `npm run build` Rebuilds all code. -- `npm run build:core` Rebuilds code in `deltachat-core-rust`. -- `npm run build:bindings` Rebuilds the bindings and links with `deltachat-core-rust`. -- `ǹpm run clean` Removes all built code -- `npm run prebuildify` Builds prebuilt binary to `prebuilds/$PLATFORM-$ARCH`. Copies `deltachat.dll` from `deltachat-core-rust` for windows. -- `npm run download-prebuilds` Downloads all prebuilt binaries from github before `npm publish`. -- `npm test` Runs `standard` and then the tests in `test/index.js`. -- `npm run test-integration` Runs the integration tests. -- `npm run hallmark` Runs `hallmark` on all markdown files. - -### Releases - -The following steps are needed to make a release: - -1. Wait until `pack-module` github action is completed -2. Run `npm publish https://download.delta.chat/node/deltachat-node-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm. - -## License - -Licensed under `GPL-3.0-or-later`, see [LICENSE](./LICENSE) file for details. - -> Copyright © 2018 `DeltaChat` contributors. -> -> This program is free software: you can redistribute it and/or modify -> it under the terms of the GNU General Public License as published by -> the Free Software Foundation, either version 3 of the License, or -> (at your option) any later version. -> -> This program is distributed in the hope that it will be useful, -> but WITHOUT ANY WARRANTY; without even the implied warranty of -> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -> GNU General Public License for more details. -> -> You should have received a copy of the GNU General Public License -> along with this program. If not, see . - -[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true - -[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8 diff --git a/node/binding.gyp b/node/binding.gyp deleted file mode 100644 index b0d92eae99..0000000000 --- a/node/binding.gyp +++ /dev/null @@ -1,78 +0,0 @@ -{ - # documentation about the format of this file can be found under https://gyp.gsrc.io/docs/InputFormatReference.md - # Variables can be specified when calling node-gyp as so: - # node-gyp configure -- -Dvarname=value - "variables": { - # Whether to use a system-wide installation of deltachat-core - # using pkg-config. Set to either "true" or "false". - "USE_SYSTEM_LIBDELTACHAT%": " { - console.log('Message sent, shutting down...') - dc.stopIO() - console.log('stopped io') - dc.unref() - }) -} - -main() diff --git a/node/images/tests.png b/node/images/tests.png deleted file mode 100644 index 829295b38d..0000000000 Binary files a/node/images/tests.png and /dev/null differ diff --git a/node/lib/binding.ts b/node/lib/binding.ts deleted file mode 100644 index 275bf12ffd..0000000000 --- a/node/lib/binding.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { join } from 'path' - -/** - * bindings are not typed yet. - * if the available function names are required they can be found inside of `../src/module.c` - */ -export const bindings: any = require('node-gyp-build')(join(__dirname, '../')) - -export default bindings diff --git a/node/lib/chat.ts b/node/lib/chat.ts deleted file mode 100644 index 49a6b8f520..0000000000 --- a/node/lib/chat.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable camelcase */ - -import binding from './binding' -import rawDebug from 'debug' -const debug = rawDebug('deltachat:node:chat') -import { C } from './constants' -import { integerToHexColor } from './util' -import { ChatJSON } from './types' - -interface NativeChat {} -/** - * Wrapper around dc_chat_t* - */ - -export class Chat { - constructor(public dc_chat: NativeChat) { - debug('Chat constructor') - if (dc_chat === null) { - throw new Error('native chat can not be null') - } - } - - getVisibility(): - | C.DC_CHAT_VISIBILITY_NORMAL - | C.DC_CHAT_VISIBILITY_ARCHIVED - | C.DC_CHAT_VISIBILITY_PINNED { - return binding.dcn_chat_get_visibility(this.dc_chat) - } - - get color(): string { - return integerToHexColor(binding.dcn_chat_get_color(this.dc_chat)) - } - - getId(): number { - return binding.dcn_chat_get_id(this.dc_chat) - } - - getName(): string { - return binding.dcn_chat_get_name(this.dc_chat) - } - - getMailinglistAddr(): string { - return binding.dcn_chat_get_mailinglist_addr(this.dc_chat) - } - - getProfileImage(): string { - return binding.dcn_chat_get_profile_image(this.dc_chat) - } - - getType(): number { - return binding.dcn_chat_get_type(this.dc_chat) - } - - isSelfTalk(): boolean { - return Boolean(binding.dcn_chat_is_self_talk(this.dc_chat)) - } - - isContactRequest(): boolean { - return Boolean(binding.dcn_chat_is_contact_request(this.dc_chat)) - } - - isUnpromoted(): boolean { - return Boolean(binding.dcn_chat_is_unpromoted(this.dc_chat)) - } - - isProtected(): boolean { - return Boolean(binding.dcn_chat_is_protected(this.dc_chat)) - } - - get canSend(): boolean { - return Boolean(binding.dcn_chat_can_send(this.dc_chat)) - } - - isDeviceTalk(): boolean { - return Boolean(binding.dcn_chat_is_device_talk(this.dc_chat)) - } - - isSingle(): boolean { - return this.getType() === C.DC_CHAT_TYPE_SINGLE - } - - isGroup(): boolean { - return this.getType() === C.DC_CHAT_TYPE_GROUP - } - - isMuted(): boolean { - return Boolean(binding.dcn_chat_is_muted(this.dc_chat)) - } - - toJson(): ChatJSON { - debug('toJson') - const visibility = this.getVisibility() - return { - archived: visibility === C.DC_CHAT_VISIBILITY_ARCHIVED, - pinned: visibility === C.DC_CHAT_VISIBILITY_PINNED, - color: this.color, - id: this.getId(), - name: this.getName(), - mailinglistAddr: this.getMailinglistAddr(), - profileImage: this.getProfileImage(), - type: this.getType(), - isSelfTalk: this.isSelfTalk(), - isUnpromoted: this.isUnpromoted(), - isProtected: this.isProtected(), - canSend: this.canSend, - isDeviceTalk: this.isDeviceTalk(), - isContactRequest: this.isContactRequest(), - muted: this.isMuted(), - } - } -} diff --git a/node/lib/chatlist.ts b/node/lib/chatlist.ts deleted file mode 100644 index 73233e0067..0000000000 --- a/node/lib/chatlist.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable camelcase */ - -import binding from './binding' -import { Lot } from './lot' -import { Chat } from './chat' -const debug = require('debug')('deltachat:node:chatlist') - -interface NativeChatList {} -/** - * Wrapper around dc_chatlist_t* - */ -export class ChatList { - constructor(private dc_chatlist: NativeChatList) { - debug('ChatList constructor') - if (dc_chatlist === null) { - throw new Error('native chat list can not be null') - } - } - - getChatId(index: number): number { - debug(`getChatId ${index}`) - return binding.dcn_chatlist_get_chat_id(this.dc_chatlist, index) - } - - getCount(): number { - debug('getCount') - return binding.dcn_chatlist_get_cnt(this.dc_chatlist) - } - - getMessageId(index: number): number { - debug(`getMessageId ${index}`) - return binding.dcn_chatlist_get_msg_id(this.dc_chatlist, index) - } - - getSummary(index: number, chat?: Chat): Lot { - debug(`getSummary ${index}`) - const dc_chat = (chat && chat.dc_chat) || null - return new Lot( - binding.dcn_chatlist_get_summary(this.dc_chatlist, index, dc_chat) - ) - } -} diff --git a/node/lib/constants.ts b/node/lib/constants.ts deleted file mode 100644 index 6c58016163..0000000000 --- a/node/lib/constants.ts +++ /dev/null @@ -1,361 +0,0 @@ -// Generated! - -export enum C { - DC_CERTCK_ACCEPT_INVALID = 2, - DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3, - DC_CERTCK_AUTO = 0, - DC_CERTCK_STRICT = 1, - DC_CHAT_ID_ALLDONE_HINT = 7, - DC_CHAT_ID_ARCHIVED_LINK = 6, - DC_CHAT_ID_LAST_SPECIAL = 9, - DC_CHAT_ID_TRASH = 3, - DC_CHAT_TYPE_BROADCAST = 160, - DC_CHAT_TYPE_GROUP = 120, - DC_CHAT_TYPE_MAILINGLIST = 140, - DC_CHAT_TYPE_SINGLE = 100, - DC_CHAT_TYPE_UNDEFINED = 0, - DC_CHAT_VISIBILITY_ARCHIVED = 1, - DC_CHAT_VISIBILITY_NORMAL = 0, - DC_CHAT_VISIBILITY_PINNED = 2, - DC_CONNECTIVITY_CONNECTED = 4000, - DC_CONNECTIVITY_CONNECTING = 2000, - DC_CONNECTIVITY_NOT_CONNECTED = 1000, - DC_CONNECTIVITY_WORKING = 3000, - DC_CONTACT_ID_DEVICE = 5, - DC_CONTACT_ID_INFO = 2, - DC_CONTACT_ID_LAST_SPECIAL = 9, - DC_CONTACT_ID_SELF = 1, - DC_DOWNLOAD_AVAILABLE = 10, - DC_DOWNLOAD_DONE = 0, - DC_DOWNLOAD_FAILURE = 20, - DC_DOWNLOAD_IN_PROGRESS = 1000, - DC_DOWNLOAD_UNDECIPHERABLE = 30, - DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200, - DC_EVENT_ACCOUNTS_CHANGED = 2302, - DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303, - DC_EVENT_CHANNEL_OVERFLOW = 2400, - DC_EVENT_CHATLIST_CHANGED = 2300, - DC_EVENT_CHATLIST_ITEM_CHANGED = 2301, - DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021, - DC_EVENT_CHAT_MODIFIED = 2020, - DC_EVENT_CONFIGURE_PROGRESS = 2041, - DC_EVENT_CONFIG_SYNCED = 2111, - DC_EVENT_CONNECTIVITY_CHANGED = 2100, - DC_EVENT_CONTACTS_CHANGED = 2030, - DC_EVENT_DELETED_BLOB_FILE = 151, - DC_EVENT_ERROR = 400, - DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410, - DC_EVENT_IMAP_CONNECTED = 102, - DC_EVENT_IMAP_INBOX_IDLE = 106, - DC_EVENT_IMAP_MESSAGE_DELETED = 104, - DC_EVENT_IMAP_MESSAGE_MOVED = 105, - DC_EVENT_IMEX_FILE_WRITTEN = 2052, - DC_EVENT_IMEX_PROGRESS = 2051, - DC_EVENT_INCOMING_MSG = 2005, - DC_EVENT_INCOMING_MSG_BUNCH = 2006, - DC_EVENT_INCOMING_REACTION = 2002, - DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003, - DC_EVENT_INFO = 100, - DC_EVENT_LOCATION_CHANGED = 2035, - DC_EVENT_MSGS_CHANGED = 2000, - DC_EVENT_MSGS_NOTICED = 2008, - DC_EVENT_MSG_DELETED = 2016, - DC_EVENT_MSG_DELIVERED = 2010, - DC_EVENT_MSG_FAILED = 2012, - DC_EVENT_MSG_READ = 2015, - DC_EVENT_NEW_BLOB_FILE = 150, - DC_EVENT_REACTIONS_CHANGED = 2001, - DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060, - DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061, - DC_EVENT_SELFAVATAR_CHANGED = 2110, - DC_EVENT_SMTP_CONNECTED = 101, - DC_EVENT_SMTP_MESSAGE_SENT = 103, - DC_EVENT_WARNING = 300, - DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121, - DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151, - DC_EVENT_WEBXDC_REALTIME_DATA = 2150, - DC_EVENT_WEBXDC_STATUS_UPDATE = 2120, - DC_GCL_ADD_ALLDONE_HINT = 4, - DC_GCL_ADD_SELF = 2, - DC_GCL_ARCHIVED_ONLY = 1, - DC_GCL_FOR_FORWARDING = 8, - DC_GCL_NO_SPECIALS = 2, - DC_GCL_VERIFIED_ONLY = 1, - DC_GCM_ADDDAYMARKER = 1, - DC_GCM_INFO_ONLY = 2, - DC_IMEX_EXPORT_BACKUP = 11, - DC_IMEX_EXPORT_SELF_KEYS = 1, - DC_IMEX_IMPORT_BACKUP = 12, - DC_IMEX_IMPORT_SELF_KEYS = 2, - DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6, - DC_INFO_EPHEMERAL_TIMER_CHANGED = 10, - DC_INFO_GROUP_IMAGE_CHANGED = 3, - DC_INFO_GROUP_NAME_CHANGED = 2, - DC_INFO_INVALID_UNENCRYPTED_MAIL = 13, - DC_INFO_LOCATIONSTREAMING_ENABLED = 8, - DC_INFO_LOCATION_ONLY = 9, - DC_INFO_MEMBER_ADDED_TO_GROUP = 4, - DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5, - DC_INFO_PROTECTION_DISABLED = 12, - DC_INFO_PROTECTION_ENABLED = 11, - DC_INFO_SECURE_JOIN_MESSAGE = 7, - DC_INFO_UNKNOWN = 0, - DC_INFO_WEBXDC_INFO_MESSAGE = 32, - DC_KEY_GEN_DEFAULT = 0, - DC_KEY_GEN_ED25519 = 2, - DC_KEY_GEN_RSA2048 = 1, - DC_KEY_GEN_RSA4096 = 3, - DC_LP_AUTH_NORMAL = 4, - DC_LP_AUTH_OAUTH2 = 2, - DC_MEDIA_QUALITY_BALANCED = 0, - DC_MEDIA_QUALITY_WORSE = 1, - DC_MSG_AUDIO = 40, - DC_MSG_FILE = 60, - DC_MSG_GIF = 21, - DC_MSG_ID_DAYMARKER = 9, - DC_MSG_ID_LAST_SPECIAL = 9, - DC_MSG_ID_MARKER1 = 1, - DC_MSG_IMAGE = 20, - DC_MSG_STICKER = 23, - DC_MSG_TEXT = 10, - DC_MSG_VCARD = 90, - DC_MSG_VIDEO = 50, - DC_MSG_VIDEOCHAT_INVITATION = 70, - DC_MSG_VOICE = 41, - DC_MSG_WEBXDC = 80, - 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, - DC_QR_ASK_VERIFYGROUP = 202, - DC_QR_BACKUP = 251, - DC_QR_BACKUP2 = 252, - DC_QR_ERROR = 400, - DC_QR_FPR_MISMATCH = 220, - DC_QR_FPR_OK = 210, - DC_QR_FPR_WITHOUT_ADDR = 230, - DC_QR_LOGIN = 520, - DC_QR_PROXY = 271, - DC_QR_REVIVE_VERIFYCONTACT = 510, - DC_QR_REVIVE_VERIFYGROUP = 512, - DC_QR_TEXT = 330, - DC_QR_URL = 332, - DC_QR_WEBRTC_INSTANCE = 260, - DC_QR_WITHDRAW_VERIFYCONTACT = 500, - DC_QR_WITHDRAW_VERIFYGROUP = 502, - DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1, - DC_SHOW_EMAILS_ALL = 2, - DC_SHOW_EMAILS_OFF = 0, - DC_SOCKET_AUTO = 0, - DC_SOCKET_PLAIN = 3, - DC_SOCKET_SSL = 1, - DC_SOCKET_STARTTLS = 2, - DC_STATE_IN_FRESH = 10, - DC_STATE_IN_NOTICED = 13, - DC_STATE_IN_SEEN = 16, - DC_STATE_OUT_DELIVERED = 26, - DC_STATE_OUT_DRAFT = 19, - DC_STATE_OUT_FAILED = 24, - DC_STATE_OUT_MDN_RCVD = 28, - DC_STATE_OUT_PENDING = 20, - DC_STATE_OUT_PREPARING = 18, - DC_STATE_UNDEFINED = 0, - DC_STR_AC_SETUP_MSG_BODY = 43, - DC_STR_AC_SETUP_MSG_SUBJECT = 42, - DC_STR_ADD_MEMBER_BY_OTHER = 129, - DC_STR_ADD_MEMBER_BY_YOU = 128, - DC_STR_AEAP_ADDR_CHANGED = 122, - DC_STR_AEAP_EXPLANATION_AND_LINK = 123, - DC_STR_ARCHIVEDCHATS = 40, - DC_STR_AUDIO = 11, - DC_STR_BACKUP_TRANSFER_MSG_BODY = 163, - DC_STR_BACKUP_TRANSFER_QR = 162, - DC_STR_BAD_TIME_MSG_BODY = 85, - DC_STR_BROADCAST_LIST = 115, - DC_STR_CANNOT_LOGIN = 60, - DC_STR_CANTDECRYPT_MSG_BODY = 29, - DC_STR_CHAT_PROTECTION_DISABLED = 171, - DC_STR_CHAT_PROTECTION_ENABLED = 170, - DC_STR_CONFIGURATION_FAILED = 84, - DC_STR_CONNECTED = 107, - DC_STR_CONNTECTING = 108, - DC_STR_CONTACT = 200, - DC_STR_CONTACT_NOT_VERIFIED = 36, - DC_STR_CONTACT_SETUP_CHANGED = 37, - DC_STR_CONTACT_VERIFIED = 35, - DC_STR_DEVICE_MESSAGES = 68, - DC_STR_DEVICE_MESSAGES_HINT = 70, - DC_STR_DOWNLOAD_AVAILABILITY = 100, - DC_STR_DRAFT = 3, - DC_STR_E2E_AVAILABLE = 25, - DC_STR_E2E_PREFERRED = 34, - DC_STR_ENCRYPTEDMSG = 24, - DC_STR_ENCR_NONE = 28, - DC_STR_ENCR_TRANSP = 27, - DC_STR_EPHEMERAL_DAY = 79, - DC_STR_EPHEMERAL_DAYS = 95, - DC_STR_EPHEMERAL_DISABLED = 75, - DC_STR_EPHEMERAL_FOUR_WEEKS = 81, - DC_STR_EPHEMERAL_HOUR = 78, - DC_STR_EPHEMERAL_HOURS = 94, - DC_STR_EPHEMERAL_MINUTE = 77, - DC_STR_EPHEMERAL_MINUTES = 93, - DC_STR_EPHEMERAL_SECONDS = 76, - DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147, - DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146, - DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145, - DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144, - DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143, - DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142, - DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149, - DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148, - DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155, - DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154, - DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139, - DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138, - DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153, - DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152, - DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151, - DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150, - DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141, - DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140, - DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157, - DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156, - DC_STR_EPHEMERAL_WEEK = 80, - DC_STR_EPHEMERAL_WEEKS = 96, - DC_STR_ERROR = 112, - DC_STR_ERROR_NO_NETWORK = 87, - DC_STR_FAILED_SENDING_TO = 74, - DC_STR_FILE = 12, - DC_STR_FINGERPRINTS = 30, - DC_STR_FORWARDED = 97, - DC_STR_GIF = 23, - DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127, - DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126, - DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135, - DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134, - DC_STR_GROUP_LEFT_BY_OTHER = 133, - DC_STR_GROUP_LEFT_BY_YOU = 132, - DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125, - DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124, - DC_STR_IMAGE = 9, - DC_STR_INCOMING_MESSAGES = 103, - DC_STR_INVALID_UNENCRYPTED_MAIL = 174, - DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111, - DC_STR_LOCATION = 66, - DC_STR_LOCATION_ENABLED_BY_OTHER = 137, - DC_STR_LOCATION_ENABLED_BY_YOU = 136, - DC_STR_MESSAGES = 114, - DC_STR_MESSAGE_ADD_MEMBER = 173, - DC_STR_MSGACTIONBYME = 63, - DC_STR_MSGACTIONBYUSER = 62, - DC_STR_MSGADDMEMBER = 17, - DC_STR_MSGDELMEMBER = 18, - DC_STR_MSGGROUPLEFT = 19, - DC_STR_MSGGRPIMGCHANGED = 16, - DC_STR_MSGGRPIMGDELETED = 33, - DC_STR_MSGGRPNAME = 15, - DC_STR_MSGLOCATIONDISABLED = 65, - DC_STR_MSGLOCATIONENABLED = 64, - DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE = 172, - DC_STR_NOMESSAGES = 1, - DC_STR_NOT_CONNECTED = 121, - DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113, - DC_STR_ONE_MOMENT = 106, - DC_STR_OUTGOING_MESSAGES = 104, - DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99, - DC_STR_PART_OF_TOTAL_USED = 116, - DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98, - DC_STR_REACTED_BY = 177, - DC_STR_READRCPT = 31, - DC_STR_READRCPT_MAILBODY = 32, - DC_STR_REMOVE_MEMBER_BY_OTHER = 131, - DC_STR_REMOVE_MEMBER_BY_YOU = 130, - DC_STR_REPLY_NOUN = 90, - DC_STR_SAVED_MESSAGES = 69, - DC_STR_SECUREJOIN_WAIT = 190, - DC_STR_SECUREJOIN_WAIT_TIMEOUT = 191, - DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120, - DC_STR_SECURE_JOIN_REPLIES = 118, - DC_STR_SECURE_JOIN_STARTED = 117, - DC_STR_SELF = 2, - DC_STR_SELF_DELETED_MSG_BODY = 91, - DC_STR_SENDING = 110, - DC_STR_SERVER_TURNED_OFF = 92, - DC_STR_SETUP_CONTACT_QR_DESC = 119, - DC_STR_STICKER = 67, - DC_STR_STORAGE_ON_DOMAIN = 105, - DC_STR_SUBJECT_FOR_NEW_CONTACT = 73, - DC_STR_SYNC_MSG_BODY = 102, - DC_STR_SYNC_MSG_SUBJECT = 101, - DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72, - DC_STR_UPDATE_REMINDER_MSG_BODY = 86, - DC_STR_UPDATING = 109, - DC_STR_VIDEO = 10, - DC_STR_VIDEOCHAT_INVITATION = 82, - DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83, - DC_STR_VOICEMESSAGE = 7, - DC_STR_WELCOME_MESSAGE = 71, - DC_STR_YOU_REACTED = 176, - DC_TEXT1_DRAFT = 1, - DC_TEXT1_SELF = 3, - DC_TEXT1_USERNAME = 2, - DC_VIDEOCHATTYPE_BASICWEBRTC = 1, - DC_VIDEOCHATTYPE_JITSI = 2, - DC_VIDEOCHATTYPE_UNKNOWN = 0, -} - -// Generated! - -export const EventId2EventName: { [key: number]: string } = { - 100: 'DC_EVENT_INFO', - 101: 'DC_EVENT_SMTP_CONNECTED', - 102: 'DC_EVENT_IMAP_CONNECTED', - 103: 'DC_EVENT_SMTP_MESSAGE_SENT', - 104: 'DC_EVENT_IMAP_MESSAGE_DELETED', - 105: 'DC_EVENT_IMAP_MESSAGE_MOVED', - 106: 'DC_EVENT_IMAP_INBOX_IDLE', - 150: 'DC_EVENT_NEW_BLOB_FILE', - 151: 'DC_EVENT_DELETED_BLOB_FILE', - 300: 'DC_EVENT_WARNING', - 400: 'DC_EVENT_ERROR', - 410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP', - 2000: 'DC_EVENT_MSGS_CHANGED', - 2001: 'DC_EVENT_REACTIONS_CHANGED', - 2002: 'DC_EVENT_INCOMING_REACTION', - 2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY', - 2005: 'DC_EVENT_INCOMING_MSG', - 2006: 'DC_EVENT_INCOMING_MSG_BUNCH', - 2008: 'DC_EVENT_MSGS_NOTICED', - 2010: 'DC_EVENT_MSG_DELIVERED', - 2012: 'DC_EVENT_MSG_FAILED', - 2015: 'DC_EVENT_MSG_READ', - 2016: 'DC_EVENT_MSG_DELETED', - 2020: 'DC_EVENT_CHAT_MODIFIED', - 2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED', - 2030: 'DC_EVENT_CONTACTS_CHANGED', - 2035: 'DC_EVENT_LOCATION_CHANGED', - 2041: 'DC_EVENT_CONFIGURE_PROGRESS', - 2051: 'DC_EVENT_IMEX_PROGRESS', - 2052: 'DC_EVENT_IMEX_FILE_WRITTEN', - 2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS', - 2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS', - 2100: 'DC_EVENT_CONNECTIVITY_CHANGED', - 2110: 'DC_EVENT_SELFAVATAR_CHANGED', - 2111: 'DC_EVENT_CONFIG_SYNCED', - 2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE', - 2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED', - 2150: 'DC_EVENT_WEBXDC_REALTIME_DATA', - 2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT', - 2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE', - 2300: 'DC_EVENT_CHATLIST_CHANGED', - 2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED', - 2302: 'DC_EVENT_ACCOUNTS_CHANGED', - 2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED', - 2400: 'DC_EVENT_CHANNEL_OVERFLOW', -} diff --git a/node/lib/contact.ts b/node/lib/contact.ts deleted file mode 100644 index 41e3e270c7..0000000000 --- a/node/lib/contact.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { integerToHexColor } from './util' - -/* eslint-disable camelcase */ - -import binding from './binding' -const debug = require('debug')('deltachat:node:contact') - -interface NativeContact {} -/** - * Wrapper around dc_contact_t* - */ -export class Contact { - constructor(public dc_contact: NativeContact) { - debug('Contact constructor') - if (dc_contact === null) { - throw new Error('native contact can not be null') - } - } - - toJson() { - debug('toJson') - return { - address: this.getAddress(), - color: this.color, - authName: this.authName, - status: this.status, - displayName: this.getDisplayName(), - id: this.getId(), - lastSeen: this.lastSeen, - name: this.getName(), - profileImage: this.getProfileImage(), - nameAndAddr: this.getNameAndAddress(), - isBlocked: this.isBlocked(), - isVerified: this.isVerified(), - } - } - - getAddress(): string { - return binding.dcn_contact_get_addr(this.dc_contact) - } - - /** Get original contact name. - * This is the name of the contact as defined by the contact themself. - * If the contact themself does not define such a name, - * an empty string is returned. */ - get authName(): string { - return binding.dcn_contact_get_auth_name(this.dc_contact) - } - - get color(): string { - return integerToHexColor(binding.dcn_contact_get_color(this.dc_contact)) - } - - /** - * contact's status - * - * Status is the last signature received in a message from this contact. - */ - get status(): string { - return binding.dcn_contact_get_status(this.dc_contact) - } - - getDisplayName(): string { - return binding.dcn_contact_get_display_name(this.dc_contact) - } - - getId(): number { - return binding.dcn_contact_get_id(this.dc_contact) - } - - get lastSeen(): number { - return binding.dcn_contact_get_last_seen(this.dc_contact) - } - - wasSeenRecently() { - return Boolean(binding.dcn_contact_was_seen_recently(this.dc_contact)) - } - - getName(): string { - return binding.dcn_contact_get_name(this.dc_contact) - } - - getNameAndAddress(): string { - return binding.dcn_contact_get_name_n_addr(this.dc_contact) - } - - getProfileImage(): string { - return binding.dcn_contact_get_profile_image(this.dc_contact) - } - - isBlocked() { - return Boolean(binding.dcn_contact_is_blocked(this.dc_contact)) - } - - isVerified() { - return Boolean(binding.dcn_contact_is_verified(this.dc_contact)) - } -} diff --git a/node/lib/context.ts b/node/lib/context.ts deleted file mode 100644 index 67055d8e5a..0000000000 --- a/node/lib/context.ts +++ /dev/null @@ -1,897 +0,0 @@ -/* eslint-disable camelcase */ - -import binding from './binding' -import { C, EventId2EventName } from './constants' -import { Chat } from './chat' -import { ChatList } from './chatlist' -import { Contact } from './contact' -import { Message } from './message' -import { Lot } from './lot' -import { Locations } from './locations' -import rawDebug from 'debug' -import { AccountManager } from './deltachat' -import { join } from 'path' -import { EventEmitter } from 'stream' -const debug = rawDebug('deltachat:node:index') - -const noop = function () {} -interface NativeContext {} - -/** - * Wrapper around dcn_context_t* - * - * only acts as event emitter when created in standalone mode (without account manager) - * with `Context.open` - */ -export class Context extends EventEmitter { - constructor( - readonly manager: AccountManager | null, - private inner_dcn_context: NativeContext, - readonly account_id: number | null - ) { - super() - debug('DeltaChat constructor') - if (inner_dcn_context === null) { - throw new Error('inner_dcn_context can not be null') - } - } - - /** Opens a standalone context (without an account manager) - * automatically starts the event handler */ - static open(cwd: string): Context { - const dbFile = join(cwd, 'db.sqlite') - const context = new Context(null, binding.dcn_context_new(dbFile), null) - debug('Opened context') - function handleCoreEvent( - eventId: number, - data1: number, - data2: number | string - ) { - const eventString = EventId2EventName[eventId] - debug(eventString, data1, data2) - if (!context.emit) { - console.log('Received an event but EventEmitter is already destroyed.') - console.log(eventString, data1, data2) - return - } - context.emit(eventString, data1, data2) - context.emit('ALL', eventString, data1, data2) - } - binding.dcn_start_event_handler( - context.dcn_context, - handleCoreEvent.bind(this) - ) - debug('Started event handler') - return context - } - - get dcn_context() { - return this.inner_dcn_context - } - - get is_open() { - return Boolean(binding.dcn_context_is_open()) - } - - open(passphrase?: string) { - return Boolean( - binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '') - ) - } - - unref() { - binding.dcn_context_unref(this.dcn_context) - ;(this.inner_dcn_context as any) = null - } - - acceptChat(chatId: number) { - binding.dcn_accept_chat(this.dcn_context, chatId) - } - - blockChat(chatId: number) { - binding.dcn_block_chat(this.dcn_context, chatId) - } - - addAddressBook(addressBook: string) { - debug(`addAddressBook ${addressBook}`) - return binding.dcn_add_address_book(this.dcn_context, addressBook) - } - - addContactToChat(chatId: number, contactId: number) { - debug(`addContactToChat ${chatId} ${contactId}`) - return Boolean( - binding.dcn_add_contact_to_chat( - this.dcn_context, - Number(chatId), - Number(contactId) - ) - ) - } - - addDeviceMessage(label: string, msg: Message | string) { - debug(`addDeviceMessage ${label} ${msg}`) - if (!msg) { - throw new Error('invalid msg parameter') - } - if (typeof label !== 'string') { - throw new Error('invalid label parameter, must be a string') - } - if (typeof msg === 'string') { - const msgObj = this.messageNew() - msgObj.setText(msg) - msg = msgObj - } - if (!msg.dc_msg) { - throw new Error('invalid msg object') - } - return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg) - } - - setChatVisibility( - chatId: number, - visibility: - | C.DC_CHAT_VISIBILITY_NORMAL - | C.DC_CHAT_VISIBILITY_ARCHIVED - | C.DC_CHAT_VISIBILITY_PINNED - ) { - debug(`setChatVisibility ${chatId} ${visibility}`) - binding.dcn_set_chat_visibility( - this.dcn_context, - Number(chatId), - visibility - ) - } - - blockContact(contactId: number, block: boolean) { - debug(`blockContact ${contactId} ${block}`) - binding.dcn_block_contact( - this.dcn_context, - Number(contactId), - block ? 1 : 0 - ) - } - - checkQrCode(qrCode: string) { - debug(`checkQrCode ${qrCode}`) - const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode) - let result = dc_lot ? new Lot(dc_lot) : null - if (result) { - return { id: result.getId(), ...result.toJson() } - } - return result - } - - configure(opts: any): Promise { - return new Promise((resolve, reject) => { - debug('configure') - - const onSuccess = () => { - removeListeners() - resolve() - } - const onFail = (error: string) => { - removeListeners() - reject(new Error(error)) - } - - let onConfigure: (...args: any[]) => void - if (this.account_id === null) { - onConfigure = (data1: number, data2: string) => { - if (data1 === 0) return onFail(data2) - else if (data1 === 1000) return onSuccess() - } - } else { - onConfigure = (accountId: number, data1: number, data2: string) => { - if (this.account_id !== accountId) { - return - } - if (data1 === 0) return onFail(data2) - else if (data1 === 1000) return onSuccess() - } - } - - const removeListeners = () => { - ;(this.manager || this).removeListener( - 'DC_EVENT_CONFIGURE_PROGRESS', - onConfigure - ) - } - - const registerListeners = () => { - ;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure) - } - - registerListeners() - - if (!opts) opts = {} - Object.keys(opts).forEach((key) => { - const value = opts[key] - this.setConfig(key, value) - }) - - binding.dcn_configure(this.dcn_context) - }) - } - - continueKeyTransfer(messageId: number, setupCode: string) { - debug(`continueKeyTransfer ${messageId}`) - return new Promise((resolve, reject) => { - binding.dcn_continue_key_transfer( - this.dcn_context, - Number(messageId), - setupCode, - (result: number) => resolve(result === 1) - ) - }) - } - /** @returns chatId */ - createBroadcastList(): number { - debug(`createBroadcastList`) - return binding.dcn_create_broadcast_list(this.dcn_context) - } - - /** @returns chatId */ - createChatByContactId(contactId: number): number { - debug(`createChatByContactId ${contactId}`) - return binding.dcn_create_chat_by_contact_id( - this.dcn_context, - Number(contactId) - ) - } - - /** @returns contactId */ - createContact(name: string, addr: string): number { - debug(`createContact ${name} ${addr}`) - return binding.dcn_create_contact(this.dcn_context, name, addr) - } - - /** - * - * @param chatName The name of the chat that should be created - * @param is_protected Whether the chat should be protected at creation time - * @returns chatId - */ - createGroupChat(chatName: string, is_protected: boolean = false): number { - debug(`createGroupChat ${chatName} [protected:${is_protected}]`) - return binding.dcn_create_group_chat( - this.dcn_context, - is_protected ? 1 : 0, - chatName - ) - } - - deleteChat(chatId: number) { - debug(`deleteChat ${chatId}`) - binding.dcn_delete_chat(this.dcn_context, Number(chatId)) - } - - deleteContact(contactId: number) { - debug(`deleteContact ${contactId}`) - return Boolean( - binding.dcn_delete_contact(this.dcn_context, Number(contactId)) - ) - } - - deleteMessages(messageIds: number[]) { - if (!Array.isArray(messageIds)) { - messageIds = [messageIds] - } - messageIds = messageIds.map((id) => Number(id)) - debug('deleteMessages', messageIds) - binding.dcn_delete_msgs(this.dcn_context, messageIds) - } - - forwardMessages(messageIds: number[], chatId: number) { - if (!Array.isArray(messageIds)) { - messageIds = [messageIds] - } - messageIds = messageIds.map((id) => Number(id)) - debug('forwardMessages', messageIds) - binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId) - } - - getBlobdir(): string { - debug('getBlobdir') - return binding.dcn_get_blobdir(this.dcn_context) - } - - getBlockedCount(): number { - debug('getBlockedCount') - return binding.dcn_get_blocked_cnt(this.dcn_context) - } - - getBlockedContacts(): number[] { - debug('getBlockedContacts') - return binding.dcn_get_blocked_contacts(this.dcn_context) - } - - getChat(chatId: number) { - debug(`getChat ${chatId}`) - const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId)) - return dc_chat ? new Chat(dc_chat) : null - } - - getChatContacts(chatId: number): number[] { - debug(`getChatContacts ${chatId}`) - return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId)) - } - - getChatIdByContactId(contactId: number): number { - debug(`getChatIdByContactId ${contactId}`) - return binding.dcn_get_chat_id_by_contact_id( - this.dcn_context, - Number(contactId) - ) - } - - getChatMedia( - chatId: number, - msgType1: number, - msgType2: number, - msgType3: number - ): number[] { - debug(`getChatMedia ${chatId}`) - return binding.dcn_get_chat_media( - this.dcn_context, - Number(chatId), - msgType1, - msgType2 || 0, - msgType3 || 0 - ) - } - - getMimeHeaders(messageId: number): string { - debug(`getMimeHeaders ${messageId}`) - return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId)) - } - - getChatlistItemSummary(chatId: number, messageId: number) { - debug(`getChatlistItemSummary ${chatId} ${messageId}`) - return new Lot( - binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId) - ) - } - - getChatMessages(chatId: number, flags: number, marker1before: number) { - debug(`getChatMessages ${chatId} ${flags} ${marker1before}`) - return binding.dcn_get_chat_msgs( - this.dcn_context, - Number(chatId), - flags, - marker1before - ) - } - - /** - * Get encryption info for a chat. - * Get a multi-line encryption info, containing encryption preferences of all members. - * Can be used to find out why messages sent to group are not encrypted. - * - * @param chatId ID of the chat to get the encryption info for. - * @return Multi-line text, must be released using dc_str_unref() after usage. - */ - getChatEncrytionInfo(chatId: number): string { - return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId) - } - - getChats(listFlags: number, queryStr: string, queryContactId: number) { - debug('getChats') - const result = [] - const list = this.getChatList(listFlags, queryStr, queryContactId) - const count = list.getCount() - for (let i = 0; i < count; i++) { - result.push(list.getChatId(i)) - } - return result - } - - getChatList(listFlags: number, queryStr: string, queryContactId: number) { - listFlags = listFlags || 0 - queryStr = queryStr || '' - queryContactId = queryContactId || 0 - debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`) - return new ChatList( - binding.dcn_get_chatlist( - this.dcn_context, - listFlags, - queryStr, - Number(queryContactId) - ) - ) - } - - getConfig(key: string): string { - debug(`getConfig ${key}`) - return binding.dcn_get_config(this.dcn_context, key) - } - - getContact(contactId: number) { - debug(`getContact ${contactId}`) - const dc_contact = binding.dcn_get_contact( - this.dcn_context, - Number(contactId) - ) - return dc_contact ? new Contact(dc_contact) : null - } - - getContactEncryptionInfo(contactId: number) { - debug(`getContactEncryptionInfo ${contactId}`) - return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId)) - } - - getContacts(listFlags: number, query: string) { - listFlags = listFlags || 0 - query = query || '' - debug(`getContacts ${listFlags} ${query}`) - return binding.dcn_get_contacts(this.dcn_context, listFlags, query) - } - - wasDeviceMessageEverAdded(label: string) { - debug(`wasDeviceMessageEverAdded ${label}`) - const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label) - return added === 1 - } - - getDraft(chatId: number) { - debug(`getDraft ${chatId}`) - const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId)) - return dc_msg ? new Message(dc_msg) : null - } - - getFreshMessageCount(chatId: number): number { - debug(`getFreshMessageCount ${chatId}`) - return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId)) - } - - getFreshMessages() { - debug('getFreshMessages') - return binding.dcn_get_fresh_msgs(this.dcn_context) - } - - getInfo() { - debug('getInfo') - const info = binding.dcn_get_info(this.dcn_context) - return AccountManager.parseGetInfo(info) - } - - getMessage(messageId: number) { - debug(`getMessage ${messageId}`) - const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId)) - return dc_msg ? new Message(dc_msg) : null - } - - getMessageCount(chatId: number): number { - debug(`getMessageCount ${chatId}`) - return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId)) - } - - getMessageInfo(messageId: number): string { - debug(`getMessageInfo ${messageId}`) - return binding.dcn_get_msg_info(this.dcn_context, Number(messageId)) - } - - getMessageHTML(messageId: number): string { - debug(`getMessageHTML ${messageId}`) - return binding.dcn_get_msg_html(this.dcn_context, Number(messageId)) - } - - getSecurejoinQrCode(chatId: number): string { - debug(`getSecurejoinQrCode ${chatId}`) - return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId)) - } - - getSecurejoinQrCodeSVG(chatId: number): string { - debug(`getSecurejoinQrCodeSVG ${chatId}`) - return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId) - } - - startIO(): void { - debug(`startIO`) - binding.dcn_start_io(this.dcn_context) - } - - stopIO(): void { - debug(`stopIO`) - binding.dcn_stop_io(this.dcn_context) - } - - stopOngoingProcess(): void { - debug(`stopOngoingProcess`) - binding.dcn_stop_ongoing_process(this.dcn_context) - } - - /** - * - * @deprecated please use `AccountManager.getSystemInfo()` instead - */ - static getSystemInfo() { - return AccountManager.getSystemInfo() - } - - getConnectivity(): number { - return binding.dcn_get_connectivity(this.dcn_context) - } - - getConnectivityHTML(): String { - return binding.dcn_get_connectivity_html(this.dcn_context) - } - - importExport(what: number, param1: string, param2 = '') { - debug(`importExport ${what} ${param1} ${param2}`) - binding.dcn_imex(this.dcn_context, what, param1, param2) - } - - importExportHasBackup(dir: string) { - debug(`importExportHasBackup ${dir}`) - return binding.dcn_imex_has_backup(this.dcn_context, dir) - } - - initiateKeyTransfer(): Promise { - return new Promise((resolve, reject) => { - debug('initiateKeyTransfer2') - binding.dcn_initiate_key_transfer(this.dcn_context, resolve) - }) - } - - isConfigured() { - debug('isConfigured') - return Boolean(binding.dcn_is_configured(this.dcn_context)) - } - - isContactInChat(chatId: number, contactId: number) { - debug(`isContactInChat ${chatId} ${contactId}`) - return Boolean( - binding.dcn_is_contact_in_chat( - this.dcn_context, - Number(chatId), - Number(contactId) - ) - ) - } - - /** - * - * @returns resulting chat id or 0 on error - */ - joinSecurejoin(qrCode: string): number { - debug(`joinSecurejoin ${qrCode}`) - return binding.dcn_join_securejoin(this.dcn_context, qrCode) - } - - lookupContactIdByAddr(addr: string): number { - debug(`lookupContactIdByAddr ${addr}`) - return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr) - } - - markNoticedChat(chatId: number) { - debug(`markNoticedChat ${chatId}`) - binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId)) - } - - markSeenMessages(messageIds: number[]) { - if (!Array.isArray(messageIds)) { - messageIds = [messageIds] - } - messageIds = messageIds.map((id) => Number(id)) - debug('markSeenMessages', messageIds) - binding.dcn_markseen_msgs(this.dcn_context, messageIds) - } - - maybeNetwork() { - debug('maybeNetwork') - binding.dcn_maybe_network(this.dcn_context) - } - - messageNew(viewType = C.DC_MSG_TEXT) { - debug(`messageNew ${viewType}`) - return new Message(binding.dcn_msg_new(this.dcn_context, viewType)) - } - - removeContactFromChat(chatId: number, contactId: number) { - debug(`removeContactFromChat ${chatId} ${contactId}`) - return Boolean( - binding.dcn_remove_contact_from_chat( - this.dcn_context, - Number(chatId), - Number(contactId) - ) - ) - } - - /** - * - * @param chatId ID of the chat to search messages in. Set this to 0 for a global search. - * @param query The query to search for. - */ - searchMessages(chatId: number, query: string): number[] { - debug(`searchMessages ${chatId} ${query}`) - return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query) - } - - sendMessage(chatId: number, msg: string | Message) { - debug(`sendMessage ${chatId}`) - if (!msg) { - throw new Error('invalid msg parameter') - } - if (typeof msg === 'string') { - const msgObj = this.messageNew() - msgObj.setText(msg) - msg = msgObj - } - if (!msg.dc_msg) { - throw new Error('invalid msg object') - } - return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg) - } - - downloadFullMessage(messageId: number) { - binding.dcn_download_full_msg(this.dcn_context, messageId) - } - - /** - * - * @returns {Promise} Promise that resolves into the resulting message id - */ - sendVideochatInvitation(chatId: number): Promise { - debug(`sendVideochatInvitation ${chatId}`) - return new Promise((resolve, reject) => { - binding.dcn_send_videochat_invitation( - this.dcn_context, - chatId, - (result: number) => { - if (result !== 0) { - resolve(result) - } else { - reject( - 'Videochatinvitation failed to send, see error events for detailed info' - ) - } - } - ) - }) - } - - setChatName(chatId: number, name: string) { - debug(`setChatName ${chatId} ${name}`) - return Boolean( - binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name) - ) - } - - getChatEphemeralTimer(chatId: number): number { - debug(`getChatEphemeralTimer ${chatId}`) - return binding.dcn_get_chat_ephemeral_timer( - this.dcn_context, - Number(chatId) - ) - } - - setChatEphemeralTimer(chatId: number, timer: number) { - debug(`setChatEphemeralTimer ${chatId} ${timer}`) - return Boolean( - binding.dcn_set_chat_ephemeral_timer( - this.dcn_context, - Number(chatId), - Number(timer) - ) - ) - } - - setChatProfileImage(chatId: number, image: string) { - debug(`setChatProfileImage ${chatId} ${image}`) - return Boolean( - binding.dcn_set_chat_profile_image( - this.dcn_context, - Number(chatId), - image || '' - ) - ) - } - - setConfig(key: string, value: string | boolean | number): number { - debug(`setConfig (string) ${key} ${value}`) - if (value === null) { - return binding.dcn_set_config_null(this.dcn_context, key) - } else { - if (typeof value === 'boolean') { - value = value === true ? '1' : '0' - } else if (typeof value === 'number') { - value = String(value) - } - return binding.dcn_set_config(this.dcn_context, key, value) - } - } - - setConfigFromQr(qrcodeContent: string): boolean { - return Boolean( - binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent) - ) - } - - estimateDeletionCount(fromServer: boolean, seconds: number): number { - debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`) - return binding.dcn_estimate_deletion_cnt( - this.dcn_context, - fromServer === true ? 1 : 0, - seconds - ) - } - - setStockTranslation(stockId: number, stockMsg: string) { - debug(`setStockTranslation ${stockId} ${stockMsg}`) - return Boolean( - binding.dcn_set_stock_translation( - this.dcn_context, - Number(stockId), - stockMsg - ) - ) - } - - setDraft(chatId: number, msg: Message | null) { - debug(`setDraft ${chatId}`) - binding.dcn_set_draft( - this.dcn_context, - Number(chatId), - msg ? msg.dc_msg : null - ) - } - - setLocation(latitude: number, longitude: number, accuracy: number) { - debug(`setLocation ${latitude}`) - binding.dcn_set_location( - this.dcn_context, - Number(latitude), - Number(longitude), - Number(accuracy) - ) - } - - /* - * @param chatId Chat-id to get location information for. - * 0 to get locations independently of the chat. - * @param contactId Contact id to get location information for. - * If also a chat-id is given, this should be a member of the given chat. - * 0 to get locations independently of the contact. - * @param timestampFrom Start of timespan to return. - * Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC. - * 0 for "start from the beginning". - * @param timestampTo End of timespan to return. - * Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC. - * 0 for "all up to now". - * @return Array of locations, NULL is never returned. - * The array is sorted descending; - * the first entry in the array is the location with the newest timestamp. - * - * Examples: - * // get locations from the last hour for a global map - * getLocations(0, 0, time(NULL)-60*60, 0); - * - * // get locations from a contact for a global map - * getLocations(0, contact_id, 0, 0); - * - * // get all locations known for a given chat - * getLocations(chat_id, 0, 0, 0); - * - * // get locations from a single contact for a given chat - * getLocations(chat_id, contact_id, 0, 0); - */ - - getLocations( - chatId: number, - contactId: number, - timestampFrom = 0, - timestampTo = 0 - ) { - const locations = new Locations( - binding.dcn_get_locations( - this.dcn_context, - Number(chatId), - Number(contactId), - timestampFrom, - timestampTo - ) - ) - return locations.toJson() - } - - /** - * - * @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds) - */ - setChatMuteDuration(chatId: number, duration: number) { - return Boolean( - binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration) - ) - } - - /** get information about the provider */ - getProviderFromEmail(email: string) { - debug('DeltaChat.getProviderFromEmail') - const provider = binding.dcn_provider_new_from_email( - this.dcn_context, - email - ) - if (!provider) { - return undefined - } - return { - before_login_hint: binding.dcn_provider_get_before_login_hint(provider), - overview_page: binding.dcn_provider_get_overview_page(provider), - status: binding.dcn_provider_get_status(provider), - } - } - - sendWebxdcStatusUpdate( - msgId: number, - json: WebxdcSendingStatusUpdate, - descr: string - ) { - return Boolean( - binding.dcn_send_webxdc_status_update( - this.dcn_context, - msgId, - JSON.stringify(json), - descr - ) - ) - } - - getWebxdcStatusUpdates( - msgId: number, - serial = 0 - ): WebxdcReceivedStatusUpdate[] { - return JSON.parse( - binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial) - ) - } - - /** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */ - getWebxdcBlob(message: Message, filename: string): Buffer | null { - return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename) - } -} - -export type WebxdcInfo = { - name: string - icon: string - summary: string - /** - * if set by the webxdc, name of the document in edit - */ - document?: string -} - -type WebxdcSendingStatusUpdate = { - /** the payload, deserialized json: - * any javascript primitive, array or object. */ - payload: T - /** optional, short, informational message that will be added to the chat, - * eg. "Alice voted" or "Bob scored 123 in MyGame"; - * usually only one line of text is shown, - * use this option sparingly to not spam the chat. */ - info?: string - /** optional, short text, shown beside app icon; - * it is recommended to use some aggregated value, - * eg. "8 votes", "Highscore: 123" */ - summary?: string - /** - * optional, name of the document in edit, - * must not be used eg. in games where the Webxdc does not create documents - */ - document?: string -} - -type WebxdcReceivedStatusUpdate = { - /** the payload, deserialized json */ - payload: T - /** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */ - serial: number - /** the maximum serial currently known. - * If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */ - max_serial: number - /** optional, short, informational message. */ - info?: string - /** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */ - summary?: string -} diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts deleted file mode 100644 index 6080aacda7..0000000000 --- a/node/lib/deltachat.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* eslint-disable camelcase */ - -import binding from './binding' -import { EventId2EventName } from './constants' -import { EventEmitter } from 'events' -import { existsSync } from 'fs' -import rawDebug from 'debug' -import { tmpdir } from 'os' -import { join } from 'path' -import { Context } from './context' -const debug = rawDebug('deltachat:node:index') - -const noop = function () {} -interface NativeAccount {} - -/** - * Wrapper around dcn_account_t* - */ -export class AccountManager extends EventEmitter { - dcn_accounts: NativeAccount - accountDir: string - jsonRpcStarted = false - - constructor(cwd: string, writable = true) { - super() - debug('DeltaChat constructor') - - this.accountDir = cwd - this.dcn_accounts = binding.dcn_accounts_new( - this.accountDir, - writable ? 1 : 0 - ) - } - - getAllAccountIds() { - return binding.dcn_accounts_get_all(this.dcn_accounts) - } - - selectAccount(account_id: number) { - return binding.dcn_accounts_select_account(this.dcn_accounts, account_id) - } - - selectedAccount(): number { - return binding.dcn_accounts_get_selected_account(this.dcn_accounts) - } - - addAccount(): number { - return binding.dcn_accounts_add_account(this.dcn_accounts) - } - - addClosedAccount(): number { - return binding.dcn_accounts_add_closed_account(this.dcn_accounts) - } - - removeAccount(account_id: number) { - return binding.dcn_accounts_remove_account(this.dcn_accounts, account_id) - } - - accountContext(account_id: number) { - const native_context = binding.dcn_accounts_get_account( - this.dcn_accounts, - account_id - ) - if (native_context === null) { - throw new Error( - `could not get context with id ${account_id}, does it even exist? please check your ids` - ) - } - return new Context(this, native_context, account_id) - } - - migrateAccount(dbfile: string): number { - return binding.dcn_accounts_migrate_account(this.dcn_accounts, dbfile) - } - - close() { - this.stopIO() - debug('unrefing context') - binding.dcn_accounts_unref(this.dcn_accounts) - debug('Unref end') - } - - emit( - event: string | symbol, - account_id: number, - data1: any, - data2: any - ): boolean { - super.emit('ALL', event, account_id, data1, data2) - return super.emit(event, account_id, data1, data2) - } - - handleCoreEvent( - eventId: number, - accountId: number, - data1: number, - data2: number | string - ) { - const eventString = EventId2EventName[eventId] - debug('event', eventString, accountId, data1, data2) - debug(eventString, data1, data2) - if (!this.emit) { - console.log('Received an event but EventEmitter is already destroyed.') - console.log(eventString, data1, data2) - return - } - this.emit(eventString, accountId, data1, data2) - } - - startEvents() { - if (this.dcn_accounts === null) { - throw new Error('dcn_account is null') - } - binding.dcn_accounts_start_event_handler( - this.dcn_accounts, - this.handleCoreEvent.bind(this) - ) - debug('Started event handler') - } - - startJsonRpcHandler(callback: ((response: string) => void) | null) { - if (this.dcn_accounts === null) { - throw new Error('dcn_account is null') - } - if (!callback) { - throw new Error('no callback set') - } - if (this.jsonRpcStarted) { - throw new Error('jsonrpc was started already') - } - - binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this)) - debug('Started JSON-RPC handler') - this.jsonRpcStarted = true - } - - jsonRpcRequest(message: string) { - if (!this.jsonRpcStarted) { - throw new Error( - 'jsonrpc is not active, start it with startJsonRpcHandler first' - ) - } - binding.dcn_json_rpc_request(this.dcn_accounts, message) - } - - startIO() { - binding.dcn_accounts_start_io(this.dcn_accounts) - } - - stopIO() { - binding.dcn_accounts_stop_io(this.dcn_accounts) - } - - static maybeValidAddr(addr: string) { - debug('DeltaChat.maybeValidAddr') - if (addr === null) return false - return Boolean(binding.dcn_maybe_valid_addr(addr)) - } - - static parseGetInfo(info: string) { - debug('static _getInfo') - const result: { [key: string]: string } = {} - - const regex = /^(\w+)=(.*)$/i - info - .split('\n') - .filter(Boolean) - .forEach((line) => { - const match = regex.exec(line) - if (match) { - result[match[1]] = match[2] - } - }) - - return result - } - - static newTemporary() { - let directory = null - while (true) { - const randomString = Math.random().toString(36).substring(2, 5) - directory = join(tmpdir(), 'deltachat-' + randomString) - if (!existsSync(directory)) break - } - const dc = new AccountManager(directory) - const accountId = dc.addAccount() - const context = dc.accountContext(accountId) - return { dc, context, accountId, directory } - } - - static getSystemInfo() { - debug('DeltaChat.getSystemInfo') - const { dc, context } = AccountManager.newTemporary() - const info = AccountManager.parseGetInfo( - binding.dcn_get_info(context.dcn_context) - ) - const { - deltachat_core_version, - sqlite_version, - sqlite_thread_safe, - libetpan_version, - openssl_version, - compile_date, - arch, - } = info - const result = { - deltachat_core_version, - sqlite_version, - sqlite_thread_safe, - libetpan_version, - openssl_version, - compile_date, - arch, - } - context.unref() - dc.close() - return result - } - - /** get information about the provider - * - * This function creates a temporary context to be standalone, - * if possible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used) - * @deprecated - */ - static getProviderFromEmail(email: string) { - debug('DeltaChat.getProviderFromEmail') - const { dc, context } = AccountManager.newTemporary() - const provider = context.getProviderFromEmail(email) - context.unref() - dc.close() - return provider - } -} diff --git a/node/lib/index.ts b/node/lib/index.ts deleted file mode 100644 index 6b179dbeb8..0000000000 --- a/node/lib/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AccountManager } from './deltachat' - -export default AccountManager - -export { Context } from './context' -export { Chat } from './chat' -export { ChatList } from './chatlist' -export { C } from './constants' -export { Contact } from './contact' -export { AccountManager as DeltaChat } -export { Locations } from './locations' -export { Lot } from './lot' -export { - Message, - MessageState, - MessageViewType, - MessageDownloadState, -} from './message' - -export * from './types' diff --git a/node/lib/locations.ts b/node/lib/locations.ts deleted file mode 100644 index 6e197a1338..0000000000 --- a/node/lib/locations.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable camelcase */ - -const binding = require('../binding') -const debug = require('debug')('deltachat:node:locations') - -interface NativeLocations {} -/** - * Wrapper around dc_location_t* - */ -export class Locations { - constructor(public dc_locations: NativeLocations) { - debug('Locations constructor') - if (dc_locations === null) { - throw new Error('dc_locations can not be null') - } - } - - locationToJson(index: number) { - debug('locationToJson') - return { - accuracy: this.getAccuracy(index), - latitude: this.getLatitude(index), - longitude: this.getLongitude(index), - timestamp: this.getTimestamp(index), - contactId: this.getContactId(index), - msgId: this.getMsgId(index), - chatId: this.getChatId(index), - isIndependent: this.isIndependent(index), - marker: this.getMarker(index), - } - } - - toJson(): ReturnType[] { - debug('toJson') - const locations = [] - const count = this.getCount() - for (let index = 0; index < count; index++) { - locations.push(this.locationToJson(index)) - } - return locations - } - - getCount(): number { - return binding.dcn_array_get_cnt(this.dc_locations) - } - - getAccuracy(index: number): number { - return binding.dcn_array_get_accuracy(this.dc_locations, index) - } - - getLatitude(index: number): number { - return binding.dcn_array_get_latitude(this.dc_locations, index) - } - - getLongitude(index: number): number { - return binding.dcn_array_get_longitude(this.dc_locations, index) - } - - getTimestamp(index: number): number { - return binding.dcn_array_get_timestamp(this.dc_locations, index) - } - - getMsgId(index: number): number { - return binding.dcn_array_get_msg_id(this.dc_locations, index) - } - - getContactId(index: number): number { - return binding.dcn_array_get_contact_id(this.dc_locations, index) - } - - getChatId(index: number): number { - return binding.dcn_array_get_chat_id(this.dc_locations, index) - } - - isIndependent(index: number): boolean { - return binding.dcn_array_is_independent(this.dc_locations, index) - } - - getMarker(index: number): string { - return binding.dcn_array_get_marker(this.dc_locations, index) - } -} diff --git a/node/lib/lot.ts b/node/lib/lot.ts deleted file mode 100644 index 4da0bccab2..0000000000 --- a/node/lib/lot.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable camelcase */ - -const binding = require('../binding') -const debug = require('debug')('deltachat:node:lot') - -interface NativeLot {} -/** - * Wrapper around dc_lot_t* - */ -export class Lot { - constructor(public dc_lot: NativeLot) { - debug('Lot constructor') - if (dc_lot === null) { - throw new Error('dc_lot can not be null') - } - } - - toJson() { - debug('toJson') - return { - state: this.getState(), - text1: this.getText1(), - text1Meaning: this.getText1Meaning(), - text2: this.getText2(), - timestamp: this.getTimestamp(), - } - } - - getId(): number { - return binding.dcn_lot_get_id(this.dc_lot) - } - - getState(): number { - return binding.dcn_lot_get_state(this.dc_lot) - } - - getText1(): string { - return binding.dcn_lot_get_text1(this.dc_lot) - } - - getText1Meaning(): string { - return binding.dcn_lot_get_text1_meaning(this.dc_lot) - } - - getText2(): string { - return binding.dcn_lot_get_text2(this.dc_lot) - } - - getTimestamp(): number { - return binding.dcn_lot_get_timestamp(this.dc_lot) - } -} diff --git a/node/lib/message.ts b/node/lib/message.ts deleted file mode 100644 index c4036ad6b4..0000000000 --- a/node/lib/message.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* eslint-disable camelcase */ - -import binding from './binding' -import { C } from './constants' -import { Lot } from './lot' -import { Chat } from './chat' -import { WebxdcInfo } from './context' -const debug = require('debug')('deltachat:node:message') - -export enum MessageDownloadState { - Available = C.DC_DOWNLOAD_AVAILABLE, - Done = C.DC_DOWNLOAD_DONE, - Failure = C.DC_DOWNLOAD_FAILURE, - InProgress = C.DC_DOWNLOAD_IN_PROGRESS, -} - -/** - * Helper class for message states so you can do e.g. - * - * if (msg.getState().isPending()) { .. } - * - */ -export class MessageState { - constructor(public state: number) { - debug(`MessageState constructor ${state}`) - } - - isUndefined() { - return this.state === C.DC_STATE_UNDEFINED - } - - isFresh() { - return this.state === C.DC_STATE_IN_FRESH - } - - isNoticed() { - return this.state === C.DC_STATE_IN_NOTICED - } - - isSeen() { - return this.state === C.DC_STATE_IN_SEEN - } - - isPending() { - return this.state === C.DC_STATE_OUT_PENDING - } - - isFailed() { - return this.state === C.DC_STATE_OUT_FAILED - } - - isDelivered() { - return this.state === C.DC_STATE_OUT_DELIVERED - } - - isReceived() { - return this.state === C.DC_STATE_OUT_MDN_RCVD - } -} - -/** - * Helper class for message types so you can do e.g. - * - * if (msg.getViewType().isVideo()) { .. } - * - */ -export class MessageViewType { - constructor(public viewType: number) { - debug(`MessageViewType constructor ${viewType}`) - } - - isText() { - return this.viewType === C.DC_MSG_TEXT - } - - isImage() { - return this.viewType === C.DC_MSG_IMAGE || this.viewType === C.DC_MSG_GIF - } - - isGif() { - return this.viewType === C.DC_MSG_GIF - } - - isAudio() { - return this.viewType === C.DC_MSG_AUDIO || this.viewType === C.DC_MSG_VOICE - } - - isVoice() { - return this.viewType === C.DC_MSG_VOICE - } - - isVideo() { - return this.viewType === C.DC_MSG_VIDEO - } - - isFile() { - return this.viewType === C.DC_MSG_FILE - } - - isVideochatInvitation() { - return this.viewType === C.DC_MSG_VIDEOCHAT_INVITATION - } -} - -interface NativeMessage {} -/** - * Wrapper around dc_msg_t* - */ -export class Message { - constructor(public dc_msg: NativeMessage) { - debug('Message constructor') - if (dc_msg === null) { - throw new Error('dc_msg can not be null') - } - } - - toJson() { - debug('toJson') - const quotedMessage = this.getQuotedMessage() - const viewType = binding.dcn_msg_get_viewtype(this.dc_msg) - return { - chatId: this.getChatId(), - webxdcInfo: viewType == C.DC_MSG_WEBXDC ? this.webxdcInfo : null, - downloadState: this.downloadState, - duration: this.getDuration(), - file: this.getFile(), - fromId: this.getFromId(), - id: this.getId(), - quotedText: this.getQuotedText(), - quotedMessageId: quotedMessage ? quotedMessage.getId() : null, - receivedTimestamp: this.getReceivedTimestamp(), - sortTimestamp: this.getSortTimestamp(), - text: this.getText(), - timestamp: this.getTimestamp(), - hasLocation: this.hasLocation(), - hasHTML: this.hasHTML, - viewType, - state: binding.dcn_msg_get_state(this.dc_msg), - hasDeviatingTimestamp: this.hasDeviatingTimestamp(), - showPadlock: this.getShowpadlock(), - summary: this.getSummary().toJson(), - subject: this.subject, - isSetupmessage: this.isSetupmessage(), - isInfo: this.isInfo(), - isForwarded: this.isForwarded(), - dimensions: { - height: this.getHeight(), - width: this.getWidth(), - }, - videochatType: this.getVideochatType(), - videochatUrl: this.getVideochatUrl(), - overrideSenderName: this.overrideSenderName, - parentId: this.parent?.getId(), - } - } - - getChatId(): number { - return binding.dcn_msg_get_chat_id(this.dc_msg) - } - - get webxdcInfo(): WebxdcInfo | null { - let info = binding.dcn_msg_get_webxdc_info(this.dc_msg) - return info - ? JSON.parse(binding.dcn_msg_get_webxdc_info(this.dc_msg)) - : null - } - - get downloadState(): MessageDownloadState { - return binding.dcn_msg_get_download_state(this.dc_msg) - } - - get parent(): Message | null { - let msg = binding.dcn_msg_get_parent(this.dc_msg) - return msg ? new Message(msg) : null - } - - getDuration(): number { - return binding.dcn_msg_get_duration(this.dc_msg) - } - - getFile(): string { - return binding.dcn_msg_get_file(this.dc_msg) - } - - getFilebytes(): number { - return binding.dcn_msg_get_filebytes(this.dc_msg) - } - - getFilemime(): string { - return binding.dcn_msg_get_filemime(this.dc_msg) - } - - getFilename(): string { - return binding.dcn_msg_get_filename(this.dc_msg) - } - - getFromId(): number { - return binding.dcn_msg_get_from_id(this.dc_msg) - } - - getHeight(): number { - return binding.dcn_msg_get_height(this.dc_msg) - } - - getId(): number { - return binding.dcn_msg_get_id(this.dc_msg) - } - - getQuotedText(): string { - return binding.dcn_msg_get_quoted_text(this.dc_msg) - } - - getQuotedMessage(): Message | null { - const dc_msg = binding.dcn_msg_get_quoted_msg(this.dc_msg) - return dc_msg ? new Message(dc_msg) : null - } - - getReceivedTimestamp(): number { - return binding.dcn_msg_get_received_timestamp(this.dc_msg) - } - - getSetupcodebegin() { - return binding.dcn_msg_get_setupcodebegin(this.dc_msg) - } - - getShowpadlock() { - return Boolean(binding.dcn_msg_get_showpadlock(this.dc_msg)) - } - - getSortTimestamp(): number { - return binding.dcn_msg_get_sort_timestamp(this.dc_msg) - } - - getState() { - return new MessageState(binding.dcn_msg_get_state(this.dc_msg)) - } - - getSummary(chat?: Chat) { - const dc_chat = (chat && chat.dc_chat) || null - return new Lot(binding.dcn_msg_get_summary(this.dc_msg, dc_chat)) - } - - get subject(): string { - return binding.dcn_msg_get_subject(this.dc_msg) - } - - getSummarytext(approxCharacters: number): string { - approxCharacters = approxCharacters || 0 - return binding.dcn_msg_get_summarytext(this.dc_msg, approxCharacters) - } - - getText(): string { - return binding.dcn_msg_get_text(this.dc_msg) - } - - getTimestamp(): number { - return binding.dcn_msg_get_timestamp(this.dc_msg) - } - - getViewType() { - return new MessageViewType(binding.dcn_msg_get_viewtype(this.dc_msg)) - } - - getVideochatType(): number { - return binding.dcn_msg_get_videochat_type(this.dc_msg) - } - - getVideochatUrl(): string { - return binding.dcn_msg_get_videochat_url(this.dc_msg) - } - - getWidth(): number { - return binding.dcn_msg_get_width(this.dc_msg) - } - - get overrideSenderName(): string { - return binding.dcn_msg_get_override_sender_name(this.dc_msg) - } - - hasDeviatingTimestamp() { - return binding.dcn_msg_has_deviating_timestamp(this.dc_msg) - } - - hasLocation() { - return Boolean(binding.dcn_msg_has_location(this.dc_msg)) - } - - get hasHTML() { - return Boolean(binding.dcn_msg_has_html(this.dc_msg)) - } - - isDeadDrop() { - // TODO: Fix - //return this.getChatId() === C.DC_CHAT_ID_DEADDROP - return false - } - - isForwarded() { - return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg)) - } - - isInfo() { - return Boolean(binding.dcn_msg_is_info(this.dc_msg)) - } - - isSent() { - return Boolean(binding.dcn_msg_is_sent(this.dc_msg)) - } - - isSetupmessage() { - return Boolean(binding.dcn_msg_is_setupmessage(this.dc_msg)) - } - - latefilingMediasize(width: number, height: number, duration: number) { - binding.dcn_msg_latefiling_mediasize(this.dc_msg, width, height, duration) - } - - setDimension(width: number, height: number) { - binding.dcn_msg_set_dimension(this.dc_msg, width, height) - return this - } - - setDuration(duration: number) { - binding.dcn_msg_set_duration(this.dc_msg, duration) - return this - } - - setFile(file: string, mime?: string) { - if (typeof file !== 'string') throw new Error('Missing filename') - binding.dcn_msg_set_file(this.dc_msg, file, mime || '') - return this - } - - setLocation(longitude: number, latitude: number) { - binding.dcn_msg_set_location(this.dc_msg, longitude, latitude) - return this - } - - setQuote(quotedMessage: Message | null) { - binding.dcn_msg_set_quote(this.dc_msg, quotedMessage?.dc_msg) - return this - } - - setText(text: string) { - binding.dcn_msg_set_text(this.dc_msg, text) - return this - } - - setHTML(html: string) { - binding.dcn_msg_set_html(this.dc_msg, html) - return this - } - - setOverrideSenderName(senderName: string) { - binding.dcn_msg_set_override_sender_name(this.dc_msg, senderName) - return this - } - - /** Force the message to be sent in plain text. - * - * This API is for bots, there is no need to expose it in the UI. - */ - forcePlaintext() { - binding.dcn_msg_force_plaintext(this.dc_msg) - } -} diff --git a/node/lib/types.ts b/node/lib/types.ts deleted file mode 100644 index 6bb54d9f86..0000000000 --- a/node/lib/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { C } from './constants' - -export type ChatTypes = - | C.DC_CHAT_TYPE_GROUP - | C.DC_CHAT_TYPE_MAILINGLIST - | C.DC_CHAT_TYPE_SINGLE - | C.DC_CHAT_TYPE_UNDEFINED - -export interface ChatJSON { - archived: boolean - pinned: boolean - color: string - id: number - name: string - mailinglistAddr: string - profileImage: string - type: number - isSelfTalk: boolean - isUnpromoted: boolean - isProtected: boolean - canSend: boolean - isDeviceTalk: boolean - isContactRequest: boolean - muted: boolean -} diff --git a/node/lib/util.ts b/node/lib/util.ts deleted file mode 100644 index 7a8c4ba6b0..0000000000 --- a/node/lib/util.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @param integerColor expects a 24bit rgb integer (left to right: 8bits red, 8bits green, 8bits blue) - */ -export function integerToHexColor(integerColor: number) { - return '#' + (integerColor + 16777216).toString(16).substring(1) -} diff --git a/node/patches/m1_build_use_x86_64.patch b/node/patches/m1_build_use_x86_64.patch deleted file mode 100644 index cf52ea2968..0000000000 --- a/node/patches/m1_build_use_x86_64.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git i/node/binding.gyp w/node/binding.gyp -index b0d92eae..c5e504fa 100644 ---- i/node/binding.gyp -+++ w/node/binding.gyp -@@ -43,7 +43,7 @@ - "include_dirs": ["../deltachat-ffi"], - "ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher - "libraries": [ -- "../../target/release/libdeltachat.a", -+ "../../target/x86_64-apple-darwin/release/libdeltachat.a", - "-ldl", - ], - "conditions": [], diff --git a/node/scripts/common.js b/node/scripts/common.js deleted file mode 100644 index af085cb591..0000000000 --- a/node/scripts/common.js +++ /dev/null @@ -1,26 +0,0 @@ -const spawnSync = require('child_process').spawnSync - -const verbose = isVerbose() - -function spawn (cmd, args, opts) { - log(`>> spawn: ${cmd} ${args.join(' ')}`) - const result = spawnSync(cmd, args, opts) - if (result.status === null) { - console.error(`Could not find ${cmd}`) - process.exit(1) - } else if (result.status !== 0) { - console.error(`${cmd} failed with code ${result.status}`) - process.exit(1) - } -} - -function log (...args) { - if (verbose) console.log(...args) -} - -function isVerbose () { - const loglevel = process.env.npm_config_loglevel - return loglevel === 'verbose' || process.env.CI === 'true' -} - -module.exports = { spawn, log, isVerbose, verbose } diff --git a/node/scripts/generate-constants.js b/node/scripts/generate-constants.js deleted file mode 100755 index 7e821dc0ad..0000000000 --- a/node/scripts/generate-constants.js +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs') -const path = require('path') - -const data = [] -const header = path.resolve(__dirname, '../../deltachat-ffi/deltachat.h') - -console.log('Generating constants...') - -const header_data = fs.readFileSync(header, 'UTF-8') -const regex = /^#define\s+(\w+)\s+(\w+)/gm -while (null != (match = regex.exec(header_data))) { - const key = match[1] - const value = parseInt(match[2]) - if (!isNaN(value)) { - data.push({ key, value }) - } -} - -delete header_data - -const constants = data - .filter( - ({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase - ) - .sort((lhs, rhs) => { - if (lhs.key < rhs.key) return -1 - else if (lhs.key > rhs.key) return 1 - return 0 - }) - .map((row) => { - return ` ${row.key}: ${row.value}` - }) - .join(',\n') - -const events = data - .sort((lhs, rhs) => { - if (lhs.value < rhs.value) return -1 - else if (lhs.value > rhs.value) return 1 - return 0 - }) - .filter((i) => { - return i.key.startsWith('DC_EVENT_') - }) - .map((i) => { - return ` ${i.value}: '${i.key}'` - }) - .join(',\n') - -// backwards compat -fs.writeFileSync( - path.resolve(__dirname, '../constants.js'), - `// Generated!\n\nmodule.exports = {\n${constants}\n}\n` -) -// backwards compat -fs.writeFileSync( - path.resolve(__dirname, '../events.js'), - `/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n` -) - -fs.writeFileSync( - path.resolve(__dirname, '../lib/constants.ts'), - `// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n -// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n` -) diff --git a/node/scripts/install.js b/node/scripts/install.js deleted file mode 100644 index 3f320be4a3..0000000000 --- a/node/scripts/install.js +++ /dev/null @@ -1,22 +0,0 @@ -const {execSync} = require('child_process') -const {existsSync} = require('fs') -const {join} = require('path') - -const run = (cmd) => { - console.log('[i] running `' + cmd + '`') - execSync(cmd, {stdio: 'inherit'}) -} - -// Build bindings -if (process.env.USE_SYSTEM_LIBDELTACHAT === 'true') { - console.log('[i] USE_SYSTEM_LIBDELTACHAT is true, rebuilding c bindings and using pkg-config to retrieve lib paths and cflags of libdeltachat') - run('npm run build:bindings:c:c') -} else { - console.log('[i] Building rust core & c bindings, if possible use prebuilds') - run('npm run install:prebuilds') -} - -if (!existsSync(join(__dirname, '..', 'dist'))) { - console.log('[i] Didn\'t find already built typescript bindings. Trying to transpile them. If this fail, make sure typescript is installed ;)') - run('npm run build:bindings:ts') -} diff --git a/node/scripts/postLinksToDetails.js b/node/scripts/postLinksToDetails.js deleted file mode 100644 index 83b70037ed..0000000000 --- a/node/scripts/postLinksToDetails.js +++ /dev/null @@ -1,47 +0,0 @@ -const { readFileSync } = require('fs') - -const sha = JSON.parse( - readFileSync(process.env['GITHUB_EVENT_PATH'], 'utf8') -).pull_request.head.sha - -const base_url = - 'https://download.delta.chat/node/' - -const GITHUB_API_URL = - 'https://api.github.com/repos/deltachat/deltachat-core-rust/statuses/' + sha - -const file_url = process.env['URL'] -const GITHUB_TOKEN = process.env['GITHUB_TOKEN'] -const context = process.env['MSG_CONTEXT'] - -const STATUS_DATA = { - state: 'success', - description: '⏩ Click on "Details" to download →', - context: context || 'Download the node-bindings.tar.gz', - target_url: base_url + file_url, -} - -const http = require('https') - -const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'github-action ci for deltachat deskop', - authorization: 'Bearer ' + GITHUB_TOKEN, - }, -} - -const req = http.request(GITHUB_API_URL, options, function(res) { - var chunks = [] - res.on('data', function(chunk) { - chunks.push(chunk) - }) - res.on('end', function() { - var body = Buffer.concat(chunks) - console.log(body.toString()) - }) -}) - -req.write(JSON.stringify(STATUS_DATA)) -req.end() diff --git a/node/scripts/postinstall.js b/node/scripts/postinstall.js deleted file mode 100644 index 15eb241ad5..0000000000 --- a/node/scripts/postinstall.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('fs') -const path = require('path') - -if (process.platform !== 'win32') { - console.log('postinstall: not windows, so skipping!') - process.exit(0) -} - -const from = path.resolve( - __dirname, - '..', - '..', - 'target', - 'release', - 'deltachat.dll' -) - -const getDestination = () => { - const argv = process.argv - if (argv.length === 3 && argv[2] === '--prebuild') { - return path.resolve( - __dirname, - '..', - 'prebuilds', - 'win32-x64', - 'deltachat.dll' - ) - } else { - return path.resolve( - __dirname, - '..', - 'build', - 'Release', - 'deltachat.dll' - ) - } -} - -const dest = getDestination() - -copy(from, dest, (err) => { - if (err) throw err - console.log(`postinstall: copied ${from} to ${dest}`) -}) - -function copy (from, to, cb) { - fs.stat(from, (err, st) => { - if (err) return cb(err) - fs.readFile(from, (err, buf) => { - if (err) return cb(err) - fs.writeFile(to, buf, (err) => { - if (err) return cb(err) - fs.chmod(to, st.mode, cb) - }) - }) - }) -} diff --git a/node/scripts/rebuild-core.js b/node/scripts/rebuild-core.js deleted file mode 100644 index 80dedcce5a..0000000000 --- a/node/scripts/rebuild-core.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path') -const { spawn } = require('./common') -const opts = { - cwd: path.resolve(__dirname, '../..'), - stdio: 'inherit' -} - -const buildArgs = [ - 'build', - '--release', - '--features', - 'vendored', - '-p', - 'deltachat_ffi' -] - -spawn('cargo', buildArgs, opts) diff --git a/node/src/module.c b/node/src/module.c deleted file mode 100644 index 02dda50c1b..0000000000 --- a/node/src/module.c +++ /dev/null @@ -1,3600 +0,0 @@ -#define NAPI_VERSION 4 - -#include -#include -#include -#include -#include -#include -#include "napi-macros-extensions.h" - -//#define DEBUG - -#ifdef DEBUG -#define TRACE(fmt, ...) fprintf(stderr, "> module.c:%d %s() " fmt "\n", __LINE__, __func__, ##__VA_ARGS__) -#else -#define TRACE(fmt, ...) -#endif - -/** - * Custom context - */ -typedef struct dcn_context_t { - dc_context_t* dc_context; - napi_threadsafe_function threadsafe_event_handler; - uv_thread_t event_handler_thread; - int gc; -} dcn_context_t; - -/** - * Custom accounts - */ -typedef struct dcn_accounts_t { - dc_accounts_t* dc_accounts; - napi_threadsafe_function threadsafe_event_handler; - uv_thread_t event_handler_thread; - napi_threadsafe_function threadsafe_jsonrpc_handler; - uv_thread_t jsonrpc_thread; - dc_jsonrpc_instance_t* jsonrpc_instance; - int gc; -} dcn_accounts_t; - - - - -/** - * Finalize functions. These are called once the corresponding - * external is garbage collected on the JavaScript side. - */ - -static void finalize_chat(napi_env env, void* data, void* hint) { - if (data) { - dc_chat_t* chat = (dc_chat_t*)data; - //TRACE("cleaning up chat %d", dc_chat_get_id(chat)); - dc_chat_unref(chat); - } -} - -static void finalize_chatlist(napi_env env, void* data, void* hint) { - if (data) { - //TRACE("cleaning up chatlist object"); - dc_chatlist_unref((dc_chatlist_t*)data); - } -} - -static void finalize_contact(napi_env env, void* data, void* hint) { - if (data) { - dc_contact_t* contact = (dc_contact_t*)data; - //TRACE("cleaning up contact %d", dc_contact_get_id(contact)); - dc_contact_unref(contact); - } -} - -static void finalize_lot(napi_env env, void* data, void* hint) { - if (data) { - //TRACE("cleaning up lot"); - dc_lot_unref((dc_lot_t*)data); - } -} - -static void finalize_array(napi_env env, void* data, void* hint) { - if (data) { - //TRACE("cleaning up array"); - dc_array_unref((dc_array_t*)data); - } -} - -static void finalize_msg(napi_env env, void* data, void* hint) { - if (data) { - dc_msg_t* msg = (dc_msg_t*)data; - //TRACE("cleaning up message %d", dc_msg_get_id(msg)); - dc_msg_unref(msg); - } -} - -static void finalize_provider(napi_env env, void* data, void* hint) { - if (data) { - dc_provider_t* provider = (dc_provider_t*)data; - //TRACE("cleaning up provider"); - dc_provider_unref(provider); - } -} - -/** - * Helpers. - */ - -static uint32_t* js_array_to_uint32(napi_env env, napi_value js_array, uint32_t* length) { - *length = 0; - NAPI_STATUS_THROWS(napi_get_array_length(env, js_array, length)); - - uint32_t* array = calloc(*length, sizeof(uint32_t)); - - for (uint32_t i = 0; i < *length; i++) { - napi_value napi_element; - NAPI_STATUS_THROWS(napi_get_element(env, js_array, i, &napi_element)); - NAPI_STATUS_THROWS(napi_get_value_uint32(env, napi_element, &array[i])); - } - - return array; -} - -static napi_value dc_array_to_js_array(napi_env env, dc_array_t* array) { - napi_value js_array; - - const int length = dc_array_get_cnt(array); - NAPI_STATUS_THROWS(napi_create_array_with_length(env, length, &js_array)); - - if (length > 0) { - for (int i = 0; i < length; i++) { - const uint32_t id = dc_array_get_id(array, i); - napi_value napi_id; - NAPI_STATUS_THROWS(napi_create_uint32(env, id, &napi_id)); - NAPI_STATUS_THROWS(napi_set_element(env, js_array, i, napi_id)); - } - } - - return js_array; -} - -/** - * Main context. - */ - -NAPI_METHOD(dcn_context_new) { - NAPI_ARGV(1); - - NAPI_ARGV_UTF8_MALLOC(db_file, 0); - - TRACE("creating new dc_context"); - - dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t)); - dcn_context->dc_context = dc_context_new(NULL, db_file, NULL); - - - napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, dcn_context, - NULL, NULL, &result)); - return result; -} - -NAPI_METHOD(dcn_context_new_closed) { - NAPI_ARGV(1); - - NAPI_ARGV_UTF8_MALLOC(db_file, 0); - - TRACE("creating new closed dc_context"); - - dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t)); - dcn_context->dc_context = dc_context_new_closed(db_file); - - - napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, dcn_context, - NULL, NULL, &result)); - return result; -} - -NAPI_METHOD(dcn_context_open) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(passphrase, 1); - - int result = dc_context_open(dcn_context->dc_context, passphrase); - free(passphrase); - - NAPI_RETURN_UINT32(result); -} - -NAPI_METHOD(dcn_context_is_open) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - int result = dc_context_is_open(dcn_context->dc_context); - - NAPI_RETURN_UINT32(result); -} - -/** - * Event struct for calling back to JavaScript - */ -typedef struct dcn_event_t { - int event; - uintptr_t data1_int; - uintptr_t data2_int; - char* data1_str; - char* data2_str; -} dcn_event_t; - - -static void event_handler_thread_func(void* arg) -{ - dcn_context_t* dcn_context = (dcn_context_t*)arg; - dc_context_t* dc_context = dcn_context->dc_context; - - - TRACE("event_handler_thread_func starting"); - - - dc_event_emitter_t* emitter = dc_get_event_emitter(dc_context); - dc_event_t* event; - while (true) { - if (emitter == NULL) { - TRACE("event emitter is null, bailing"); - break; - } - - event = dc_get_next_event(emitter); - if (event == NULL) { - TRACE("event is null, bailing"); - break; - } - - if (!dcn_context->threadsafe_event_handler) { - TRACE("threadsafe_event_handler not set, bailing"); - break; - } - - // Don't process events if we're being garbage collected! - if (dcn_context->gc == 1) { - TRACE("dc_context has been destroyed, bailing"); - break; - } - - - napi_status status = napi_call_threadsafe_function(dcn_context->threadsafe_event_handler, event, napi_tsfn_blocking); - - if (status == napi_closing) { - TRACE("JS function got released, bailing"); - break; - } - } - - dc_event_emitter_unref(emitter); - - TRACE("event_handler_thread_func ended"); - - napi_release_threadsafe_function(dcn_context->threadsafe_event_handler, napi_tsfn_release); -} - -static void call_js_event_handler(napi_env env, napi_value js_callback, void* _context, void* data) -{ - dc_event_t* dc_event = (dc_event_t*)data; - - napi_value global; - napi_status status = napi_get_global(env, &global); - - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to get global"); - } - -#define CALL_JS_CALLBACK_ARGC 3 - - const int argc = CALL_JS_CALLBACK_ARGC; - napi_value argv[CALL_JS_CALLBACK_ARGC]; - - const int event_id = dc_event_get_id(dc_event); - - status = napi_create_int32(env, event_id, &argv[0]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[0] for event_handler arguments"); - } - - status = napi_create_int32(env, dc_event_get_data1_int(dc_event), &argv[1]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[1] for event_handler arguments"); - } - - if DC_EVENT_DATA2_IS_STRING(event_id) { - char* data2_string = dc_event_get_data2_str(dc_event); - // Quick fix for https://github.com/deltachat/deltachat-core-rust/issues/1949 - if (data2_string != 0) { - status = napi_create_string_utf8(env, data2_string, NAPI_AUTO_LENGTH, &argv[2]); - } else { - status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[2]); - } - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments"); - } - free(data2_string); - } else { - status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[2]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments"); - } - } - - dc_event_unref(dc_event); - dc_event = NULL; - - TRACE("calling back into js"); - - napi_value result; - status = napi_call_function( - env, - global, - js_callback, - argc, - argv, - &result); - - if (status != napi_ok) { - TRACE("Unable to call event_handler callback2"); - const napi_extended_error_info* error_result; - NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result)); - } -} - - -NAPI_METHOD(dcn_start_event_handler) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - napi_value callback = argv[1]; - - TRACE("calling.."); - napi_value async_resource_name; - NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_event_callback", NAPI_AUTO_LENGTH, &async_resource_name)); - - TRACE("creating threadsafe function.."); - - NAPI_STATUS_THROWS(napi_create_threadsafe_function( - env, - callback, - 0, - async_resource_name, - 1000, // max_queue_size - 1, - NULL, - NULL, - dcn_context, - call_js_event_handler, - &dcn_context->threadsafe_event_handler)); - TRACE("done"); - - dcn_context->gc = 0; - TRACE("creating uv thread.."); - uv_thread_create(&dcn_context->event_handler_thread, event_handler_thread_func, dcn_context); - - NAPI_RETURN_UNDEFINED(); -} - - -NAPI_METHOD(dcn_context_unref) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - TRACE("Unrefing dc_context"); - dcn_context->gc = 1; - if (dcn_context->event_handler_thread != 0) { - dc_stop_io(dcn_context->dc_context); - uv_thread_join(&dcn_context->event_handler_thread); - dcn_context->event_handler_thread = 0; - } - dc_context_unref(dcn_context->dc_context); - dcn_context->dc_context = NULL; - - NAPI_RETURN_UNDEFINED(); - -} - -/** - * Static functions - */ - -NAPI_METHOD(dcn_maybe_valid_addr) { - NAPI_ARGV(1); - NAPI_ARGV_UTF8_MALLOC(addr, 0); - - //TRACE("calling.."); - int result = dc_may_be_valid_addr(addr); - //TRACE("result %d", result); - - free(addr); - - NAPI_RETURN_INT32(result); -} - -/** - * dcn_context_t - */ - -NAPI_METHOD(dcn_add_address_book) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(address_book, 1); - - //TRACE("calling.."); - int result = dc_add_address_book(dcn_context->dc_context, address_book); - //TRACE("result %d", result); - - free(address_book); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_add_contact_to_chat) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UINT32(contact_id, 2); - - //TRACE("calling.."); - int result = dc_add_contact_to_chat(dcn_context->dc_context, - chat_id, contact_id); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_add_device_msg) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - - NAPI_ARGV_UTF8_MALLOC(label, 1); - - //TRACE("calling.."); - dc_msg_t* dc_msg = NULL; - napi_get_value_external(env, argv[2], (void**)&dc_msg); - - uint32_t msg_id = dc_add_device_msg(dcn_context->dc_context, label, dc_msg); - - free(label); - //TRACE("done"); - - NAPI_RETURN_UINT32(msg_id); -} - -NAPI_METHOD(dcn_block_contact) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(contact_id, 1); - NAPI_ARGV_INT32(new_blocking, 2); - - //TRACE("calling.."); - dc_block_contact(dcn_context->dc_context, contact_id, new_blocking); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_check_qr) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(qr, 1); - - //TRACE("calling.."); - dc_lot_t* lot = dc_check_qr(dcn_context->dc_context, qr); - - free(qr); - - napi_value result; - if (lot == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, lot, - finalize_lot, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - - -NAPI_METHOD(dcn_configure) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - TRACE("calling.."); - dc_configure(dcn_context->dc_context); - TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_accept_chat) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - dc_accept_chat(dcn_context->dc_context, chat_id); - - NAPI_RETURN_UNDEFINED() -} - -NAPI_METHOD(dcn_block_chat) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - dc_block_chat(dcn_context->dc_context, chat_id); - - NAPI_RETURN_UNDEFINED() -} - -NAPI_ASYNC_CARRIER_BEGIN(dcn_continue_key_transfer) - int msg_id; - char* setup_code; - int result; -NAPI_ASYNC_CARRIER_END(dcn_continue_key_transfer) - - -NAPI_ASYNC_EXECUTE(dcn_continue_key_transfer) { - NAPI_ASYNC_GET_CARRIER(dcn_continue_key_transfer) - carrier->result = dc_continue_key_transfer(carrier->dcn_context->dc_context, - carrier->msg_id, carrier->setup_code); -} - -NAPI_ASYNC_COMPLETE(dcn_continue_key_transfer) { - NAPI_ASYNC_GET_CARRIER(dcn_continue_key_transfer) - if (status != napi_ok) { - napi_throw_type_error(env, NULL, "Execute callback failed."); - return; - } - -#define DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC 1 - - const int argc = DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC; - napi_value argv[DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC]; - NAPI_STATUS_THROWS(napi_create_int32(env, carrier->result, &argv[0])); - - NAPI_ASYNC_CALL_AND_DELETE_CB() - dc_str_unref(carrier->setup_code); - free(carrier); -} - -NAPI_METHOD(dcn_continue_key_transfer) { - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - NAPI_ARGV_UTF8_MALLOC(setup_code, 2); - NAPI_ASYNC_NEW_CARRIER(dcn_continue_key_transfer); - carrier->msg_id = msg_id; - carrier->setup_code = setup_code; - - NAPI_ASYNC_QUEUE_WORK(dcn_continue_key_transfer, argv[3]); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_join_securejoin) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(qr_code, 1); - - uint32_t chat_id = dc_join_securejoin(dcn_context->dc_context, qr_code); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_create_chat_by_contact_id) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(contact_id, 1); - - //TRACE("calling.."); - uint32_t chat_id = dc_create_chat_by_contact_id(dcn_context->dc_context, contact_id); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_create_broadcast_list) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - uint32_t chat_id = dc_create_broadcast_list(dcn_context->dc_context); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_create_contact) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(name, 1); - NAPI_ARGV_UTF8_MALLOC(addr, 2); - - //TRACE("calling.."); - uint32_t contact_id = dc_create_contact(dcn_context->dc_context, name, addr); - //TRACE("result %d", contact_id); - - free(name); - free(addr); - - NAPI_RETURN_UINT32(contact_id); -} - -NAPI_METHOD(dcn_create_group_chat) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(protect, 1); - NAPI_ARGV_UTF8_MALLOC(chat_name, 2); - - //TRACE("calling.."); - uint32_t chat_id = dc_create_group_chat(dcn_context->dc_context, protect, chat_name); - //TRACE("result %d", chat_id); - - free(chat_name); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_delete_chat) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - dc_delete_chat(dcn_context->dc_context, chat_id); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_delete_contact) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(contact_id, 1); - - //TRACE("calling.."); - int result = dc_delete_contact(dcn_context->dc_context, contact_id); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_delete_msgs) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - napi_value js_array = argv[1]; - - //TRACE("calling.."); - uint32_t length; - uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length); - dc_delete_msgs(dcn_context->dc_context, msg_ids, length); - free(msg_ids); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_forward_msgs) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - napi_value js_array = argv[1]; - NAPI_ARGV_UINT32(chat_id, 2); - - //TRACE("calling.."); - uint32_t length; - uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length); - dc_forward_msgs(dcn_context->dc_context, msg_ids, length, chat_id); - free(msg_ids); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_get_blobdir) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - char* blobdir = dc_get_blobdir(dcn_context->dc_context); - //TRACE("result %s", blobdir); - - NAPI_RETURN_AND_UNREF_STRING(blobdir); -} - -NAPI_METHOD(dcn_get_blocked_cnt) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - int blocked_cnt = dc_get_blocked_cnt(dcn_context->dc_context); - //TRACE("result %d", blocked_cnt); - - NAPI_RETURN_INT32(blocked_cnt); -} - -NAPI_METHOD(dcn_get_blocked_contacts) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - dc_array_t* contacts = dc_get_blocked_contacts(dcn_context->dc_context); - napi_value js_array = dc_array_to_js_array(env, contacts); - dc_array_unref(contacts); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_chat) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - napi_value result; - dc_chat_t* chat = dc_get_chat(dcn_context->dc_context, chat_id); - - if (chat == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, chat, finalize_chat, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_get_chat_contacts) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - dc_array_t* contacts = dc_get_chat_contacts(dcn_context->dc_context, chat_id); - napi_value js_array = dc_array_to_js_array(env, contacts); - dc_array_unref(contacts); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_chat_encrinfo) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - char *value = dc_get_chat_encrinfo(dcn_context->dc_context, chat_id); - NAPI_RETURN_AND_UNREF_STRING(value); -} - -NAPI_METHOD(dcn_get_chat_id_by_contact_id) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(contact_id, 1); - - //TRACE("calling.."); - uint32_t chat_id = dc_get_chat_id_by_contact_id(dcn_context->dc_context, - contact_id); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_get_chat_media) { - NAPI_ARGV(5); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_INT32(msg_type1, 2); - NAPI_ARGV_INT32(msg_type2, 3); - NAPI_ARGV_INT32(msg_type3, 4); - - //TRACE("calling.."); - dc_array_t* msg_ids = dc_get_chat_media(dcn_context->dc_context, - chat_id, - msg_type1, - msg_type2, - msg_type3); - napi_value js_array = dc_array_to_js_array(env, msg_ids); - dc_array_unref(msg_ids); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_mime_headers) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - - //TRACE("calling.."); - char* headers = dc_get_mime_headers(dcn_context->dc_context, msg_id); - //TRACE("result %s", headers); - - NAPI_RETURN_AND_UNREF_STRING(headers); -} - -NAPI_METHOD(dcn_get_chat_msgs) { - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UINT32(flags, 2); - NAPI_ARGV_UINT32(marker1before, 3); - - //TRACE("calling.."); - dc_array_t* msg_ids = dc_get_chat_msgs(dcn_context->dc_context, - chat_id, - flags, - marker1before); - napi_value js_array = dc_array_to_js_array(env, msg_ids); - dc_array_unref(msg_ids); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_chatlist) { - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(listflags, 1); - NAPI_ARGV_UTF8_MALLOC(query, 2); - NAPI_ARGV_UINT32(query_contact_id, 3); - - //TRACE("calling.."); - dc_chatlist_t* chatlist = dc_get_chatlist(dcn_context->dc_context, - listflags, - query && query[0] ? query : NULL, - query_contact_id); - - free(query); - - napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, - chatlist, - finalize_chatlist, - NULL, - &result)); - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_get_config) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(key, 1); - - //TRACE("calling.."); - char *value = dc_get_config(dcn_context->dc_context, key); - //TRACE("result %s", value); - - free(key); - - NAPI_RETURN_AND_UNREF_STRING(value); -} - -NAPI_METHOD(dcn_get_contact) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(contact_id, 1); - - //TRACE("calling.."); - napi_value result; - dc_contact_t* contact = dc_get_contact(dcn_context->dc_context, contact_id); - - if (contact == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, contact, - finalize_contact, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_get_contact_encrinfo) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(contact_id, 1); - - //TRACE("calling.."); - char* encr_info = dc_get_contact_encrinfo(dcn_context->dc_context, - contact_id); - //TRACE("result %s", encr_info); - - NAPI_RETURN_AND_UNREF_STRING(encr_info); -} - -NAPI_METHOD(dcn_get_contacts) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(listflags, 1); - NAPI_ARGV_UTF8_MALLOC(query, 2); - - //TRACE("calling.."); - dc_array_t* contacts = dc_get_contacts(dcn_context->dc_context, listflags, - query && query[0] ? query : NULL); - napi_value js_array = dc_array_to_js_array(env, contacts); - free(query); - dc_array_unref(contacts); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_connectivity) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - int connectivity = dc_get_connectivity(dcn_context->dc_context); - NAPI_RETURN_INT32(connectivity); -} - -NAPI_METHOD(dcn_get_connectivity_html) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - char* connectivity = dc_get_connectivity_html(dcn_context->dc_context); - NAPI_RETURN_AND_UNREF_STRING(connectivity); -} - -NAPI_METHOD(dcn_was_device_msg_ever_added) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - - NAPI_ARGV_UTF8_MALLOC(label, 1); - - //TRACE("calling.."); - - uint32_t added = dc_was_device_msg_ever_added(dcn_context->dc_context, label); - - free(label); - //TRACE("done"); - - NAPI_RETURN_UINT32(added); -} - -NAPI_METHOD(dcn_get_draft) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - napi_value result; - dc_msg_t* draft = dc_get_draft(dcn_context->dc_context, chat_id); - - if (draft == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, draft, finalize_msg, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_get_fresh_msg_cnt) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - int msg_cnt = dc_get_fresh_msg_cnt(dcn_context->dc_context, chat_id); - //TRACE("result %d", msg_cnt); - - NAPI_RETURN_INT32(msg_cnt); -} - -NAPI_METHOD(dcn_get_fresh_msgs) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - dc_array_t* msg_ids = dc_get_fresh_msgs(dcn_context->dc_context); - napi_value js_array = dc_array_to_js_array(env, msg_ids); - dc_array_unref(msg_ids); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_get_info) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - char *str = dc_get_info(dcn_context->dc_context); - //TRACE("result %s", str); - - NAPI_RETURN_AND_UNREF_STRING(str); -} - -NAPI_METHOD(dcn_get_msg) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - - //TRACE("calling.."); - napi_value result; - dc_msg_t* msg = dc_get_msg(dcn_context->dc_context, msg_id); - - if (msg == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_get_msg_cnt) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - int msg_cnt = dc_get_msg_cnt(dcn_context->dc_context, chat_id); - //TRACE("result %d", msg_cnt); - - NAPI_RETURN_INT32(msg_cnt); -} - -NAPI_METHOD(dcn_get_msg_info) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - - //TRACE("calling.."); - char* msg_info = dc_get_msg_info(dcn_context->dc_context, msg_id); - //TRACE("result %s", msg_info); - - NAPI_RETURN_AND_UNREF_STRING(msg_info); -} - - -NAPI_METHOD(dcn_get_msg_html) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - - //TRACE("calling.."); - char* msg_html = dc_get_msg_html(dcn_context->dc_context, msg_id); - //TRACE("result %s", msg_html); - - NAPI_RETURN_AND_UNREF_STRING(msg_html); -} - -NAPI_METHOD(dcn_set_chat_visibility) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_INT32(visibility, 2); - //TRACE("calling.."); - dc_set_chat_visibility(dcn_context->dc_context, - chat_id, - visibility); - //TRACE("result %d", next_id); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_get_securejoin_qr) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(group_chat_id, 1); - - //TRACE("calling.."); - char* code = dc_get_securejoin_qr(dcn_context->dc_context, - group_chat_id); - //TRACE("result %s", code); - - NAPI_RETURN_AND_UNREF_STRING(code); -} - -NAPI_METHOD(dcn_get_securejoin_qr_svg) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(group_chat_id, 1); - - //TRACE("calling.."); - char* svg = dc_get_securejoin_qr_svg(dcn_context->dc_context, group_chat_id); - //TRACE("result %s", code); - - NAPI_RETURN_AND_UNREF_STRING(svg); -} - -NAPI_METHOD(dcn_imex) { - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(what, 1); - NAPI_ARGV_UTF8_MALLOC(param1, 2); - NAPI_ARGV_UTF8_MALLOC(param2, 3); - - TRACE("calling.."); - dc_imex(dcn_context->dc_context, - what, - param1, - param2 && param2[0] ? param2 : NULL); - - free(param1); - free(param2); - TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_imex_has_backup) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(dir_name, 1); - - //TRACE("calling.."); - char* file = dc_imex_has_backup(dcn_context->dc_context, dir_name); - //TRACE("result %s", file); - - free(dir_name); - - NAPI_RETURN_AND_UNREF_STRING(file); -} - -NAPI_ASYNC_CARRIER_BEGIN(dcn_initiate_key_transfer) - char* result; -NAPI_ASYNC_CARRIER_END(dcn_initiate_key_transfer) - -NAPI_ASYNC_EXECUTE(dcn_initiate_key_transfer) { - NAPI_ASYNC_GET_CARRIER(dcn_initiate_key_transfer); - carrier->result = dc_initiate_key_transfer(carrier->dcn_context->dc_context); -} - -NAPI_ASYNC_COMPLETE(dcn_initiate_key_transfer) { - NAPI_ASYNC_GET_CARRIER(dcn_initiate_key_transfer); - if (status != napi_ok) { - napi_throw_type_error(env, NULL, "Execute callback failed."); - return; - } - -#define DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC 1 - - const int argc = DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC; - napi_value argv[DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC]; - - if (carrier->result) { - NAPI_STATUS_THROWS(napi_create_string_utf8(env, carrier->result, NAPI_AUTO_LENGTH, &argv[0])); - } else { - NAPI_STATUS_THROWS(napi_get_null(env, &argv[0])); - } - - NAPI_ASYNC_CALL_AND_DELETE_CB(); - dc_str_unref(carrier->result); - free(carrier); -} - -NAPI_METHOD(dcn_initiate_key_transfer) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - - NAPI_ASYNC_NEW_CARRIER(dcn_initiate_key_transfer); - - NAPI_ASYNC_QUEUE_WORK(dcn_initiate_key_transfer, argv[1]); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_is_configured) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - int result = dc_is_configured(dcn_context->dc_context); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_is_contact_in_chat) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UINT32(contact_id, 2); - - //TRACE("calling.."); - int result = dc_is_contact_in_chat(dcn_context->dc_context, - chat_id, contact_id); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_lookup_contact_id_by_addr) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(addr, 1); - - //TRACE("calling.."); - uint32_t res = dc_lookup_contact_id_by_addr(dcn_context->dc_context, addr); - //TRACE("result %d", res); - - free(addr); - - NAPI_RETURN_UINT32(res); -} - -NAPI_METHOD(dcn_marknoticed_chat) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - dc_marknoticed_chat(dcn_context->dc_context, chat_id); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_download_full_msg) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - - //TRACE("calling.."); - dc_download_full_msg(dcn_context->dc_context, msg_id); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_markseen_msgs) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - napi_value js_array = argv[1]; - - //TRACE("calling.."); - uint32_t length; - uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length); - dc_markseen_msgs(dcn_context->dc_context, msg_ids, length); - free(msg_ids); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_maybe_network) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - dc_maybe_network(dcn_context->dc_context); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_new) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(viewtype, 1); - - //TRACE("calling.."); - napi_value result; - dc_msg_t* msg = dc_msg_new(dcn_context->dc_context, viewtype); - - NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg, - NULL, &result)); - //TRACE("done"); - - return result; -} - - -NAPI_METHOD(dcn_remove_contact_from_chat) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UINT32(contact_id, 2); - - //TRACE("calling.."); - int result = dc_remove_contact_from_chat(dcn_context->dc_context, - chat_id, contact_id); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_search_msgs) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UTF8_MALLOC(query, 2); - - //TRACE("calling.."); - dc_array_t* msg_ids = dc_search_msgs(dcn_context->dc_context, - chat_id, query); - napi_value js_array = dc_array_to_js_array(env, msg_ids); - dc_array_unref(msg_ids); - free(query); - //TRACE("done"); - - return js_array; -} - -NAPI_METHOD(dcn_send_msg) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - dc_msg_t* dc_msg = NULL; - napi_get_value_external(env, argv[2], (void**)&dc_msg); - - uint32_t msg_id = dc_send_msg(dcn_context->dc_context, chat_id, dc_msg); - //TRACE("done"); - - NAPI_RETURN_UINT32(msg_id); -} - -NAPI_ASYNC_CARRIER_BEGIN(dcn_send_videochat_invitation) - int chat_id; - int result; -NAPI_ASYNC_CARRIER_END(dcn_send_videochat_invitation) - -NAPI_ASYNC_EXECUTE(dcn_send_videochat_invitation) { - NAPI_ASYNC_GET_CARRIER(dcn_send_videochat_invitation) - carrier->result = dc_send_videochat_invitation( - carrier->dcn_context->dc_context, - carrier->chat_id - ); -} - -NAPI_ASYNC_COMPLETE(dcn_send_videochat_invitation) { - NAPI_ASYNC_GET_CARRIER(dcn_send_videochat_invitation) - if (status != napi_ok) { - napi_throw_type_error(env, NULL, "Execute callback failed."); - return; - } - -#define DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC 1 - - const int argc = DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC; - napi_value argv[DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC]; - NAPI_STATUS_THROWS(napi_create_int32(env, carrier->result, &argv[0])); - - NAPI_ASYNC_CALL_AND_DELETE_CB() - free(carrier); -} - -NAPI_METHOD(dcn_send_videochat_invitation) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ASYNC_NEW_CARRIER(dcn_send_videochat_invitation); - carrier->chat_id = chat_id; - - NAPI_ASYNC_QUEUE_WORK(dcn_send_videochat_invitation, argv[2]); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_set_chat_name) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UTF8_MALLOC(name, 2); - - //TRACE("calling.."); - int result = dc_set_chat_name(dcn_context->dc_context, - chat_id, - name); - //TRACE("result %d", result); - - free(name); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_get_chat_ephemeral_timer) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - uint32_t result = dc_get_chat_ephemeral_timer(dcn_context->dc_context, - chat_id); - NAPI_RETURN_UINT32(result); -} - -NAPI_METHOD(dcn_set_chat_ephemeral_timer) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UINT32(timer, 2); - - int result = dc_set_chat_ephemeral_timer(dcn_context->dc_context, - chat_id, - timer); - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_set_chat_profile_image) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_UTF8_MALLOC(image, 2); - - //TRACE("calling.."); - int result = dc_set_chat_profile_image(dcn_context->dc_context, - chat_id, - image && image[0] ? image : NULL); - //TRACE("result %d", result); - - free(image); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_set_chat_mute_duration) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_INT32(duration, 2); - - //TRACE("calling.."); - int result = dc_set_chat_mute_duration(dcn_context->dc_context, - chat_id, - duration); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_set_config) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(key, 1); - NAPI_ARGV_UTF8_MALLOC(value, 2); - - //TRACE("calling.."); - int status = dc_set_config(dcn_context->dc_context, key, value); - //TRACE("result %d", status); - - free(key); - free(value); - - NAPI_RETURN_INT32(status); -} - -NAPI_METHOD(dcn_set_config_null) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(key, 1); - - //TRACE("calling.."); - int status = dc_set_config(dcn_context->dc_context, key, NULL); - //TRACE("result %d", status); - - free(key); - - NAPI_RETURN_INT32(status); -} - -NAPI_METHOD(dcn_set_config_from_qr) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(qr, 1); - - //TRACE("calling.."); - int status = dc_set_config_from_qr(dcn_context->dc_context, qr); - //TRACE("result %d", status); - - free(qr); - - NAPI_RETURN_INT32(status); -} - -NAPI_METHOD(dcn_estimate_deletion_cnt) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(from_server, 1); - NAPI_ARGV_INT32(seconds, 2); - - int result = dc_estimate_deletion_cnt (dcn_context->dc_context, from_server, seconds); - - NAPI_RETURN_INT32(result); -} - - -NAPI_METHOD(dcn_set_draft) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - - //TRACE("calling.."); - dc_msg_t* dc_msg = NULL; - napi_get_value_external(env, argv[2], (void**)&dc_msg); - - dc_set_draft(dcn_context->dc_context, chat_id, dc_msg); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_set_stock_translation) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(stock_id, 1); - NAPI_ARGV_UTF8_MALLOC(stock_msg, 2); - - int result = dc_set_stock_translation(dcn_context->dc_context, stock_id, stock_msg); - free(stock_msg); - NAPI_RETURN_INT32(result); -} - - -NAPI_METHOD(dcn_start_io) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - TRACE("calling.."); - TRACE("done"); - - dc_start_io(dcn_context->dc_context); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_stop_io) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - dc_stop_io(dcn_context->dc_context); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_stop_ongoing_process) { - NAPI_ARGV(1); - NAPI_DCN_CONTEXT(); - - //TRACE("calling.."); - dc_stop_ongoing_process(dcn_context->dc_context); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -/** - * dc_chat_t - */ - -NAPI_METHOD(dcn_chat_get_color) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - uint32_t color = dc_chat_get_color(dc_chat); - //TRACE("result %d", color); - - NAPI_RETURN_UINT32(color); -} - -NAPI_METHOD(dcn_chat_get_visibility) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - uint32_t visibility = dc_chat_get_visibility(dc_chat); - //TRACE("result %d", color); - - NAPI_RETURN_UINT32(visibility); -} - -NAPI_METHOD(dcn_chat_get_id) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - uint32_t chat_id = dc_chat_get_id(dc_chat); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_chat_get_name) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - char* name = dc_chat_get_name(dc_chat); - //TRACE("result %s", name); - - NAPI_RETURN_AND_UNREF_STRING(name); -} - -NAPI_METHOD(dcn_chat_get_mailinglist_addr) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - char* addr = dc_chat_get_mailinglist_addr(dc_chat); - //TRACE("result %s", name); - - NAPI_RETURN_AND_UNREF_STRING(addr); -} - - -NAPI_METHOD(dcn_chat_get_profile_image) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - char* profile_image = dc_chat_get_profile_image(dc_chat); - //TRACE("result %s", profile_image); - - NAPI_RETURN_AND_UNREF_STRING(profile_image); -} - -NAPI_METHOD(dcn_chat_get_type) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int type = dc_chat_get_type(dc_chat); - //TRACE("result %d", type); - - NAPI_RETURN_INT32(type); -} - -NAPI_METHOD(dcn_chat_is_self_talk) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_self_talk = dc_chat_is_self_talk(dc_chat); - //TRACE("result %d", is_self_talk); - - NAPI_RETURN_INT32(is_self_talk); -} - -NAPI_METHOD(dcn_chat_is_unpromoted) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_unpromoted = dc_chat_is_unpromoted(dc_chat); - //TRACE("result %d", is_unpromoted); - - NAPI_RETURN_INT32(is_unpromoted); -} - -NAPI_METHOD(dcn_chat_can_send) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int can_send = dc_chat_can_send(dc_chat); - //TRACE("result %d", can_send); - - NAPI_RETURN_INT32(can_send); -} - -NAPI_METHOD(dcn_chat_is_protected) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_protected = dc_chat_is_protected(dc_chat); - //TRACE("result %d", is_protected); - - NAPI_RETURN_INT32(is_protected); -} - -NAPI_METHOD(dcn_chat_is_device_talk) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_device_talk = dc_chat_is_device_talk(dc_chat); - //TRACE("result %d", is_device_talk); - - NAPI_RETURN_INT32(is_device_talk); -} - -NAPI_METHOD(dcn_chat_is_muted) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_muted = dc_chat_is_muted(dc_chat); - //TRACE("result %d", is_muted); - - NAPI_RETURN_INT32(is_muted); -} - -NAPI_METHOD(dcn_chat_is_contact_request) { - NAPI_ARGV(1); - NAPI_DC_CHAT(); - - //TRACE("calling.."); - int is_contact_request = dc_chat_is_contact_request(dc_chat); - //TRACE("result %d", is_muted); - - NAPI_RETURN_INT32(is_contact_request); -} - - - -/** - * dc_chatlist_t - */ - -NAPI_METHOD(dcn_chatlist_get_chat_id) { - NAPI_ARGV(2); - NAPI_DC_CHATLIST(); - NAPI_ARGV_INT32(index, 1); - - //TRACE("calling.."); - uint32_t chat_id = dc_chatlist_get_chat_id(dc_chatlist, index); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_chatlist_get_cnt) { - NAPI_ARGV(1); - NAPI_DC_CHATLIST(); - - //TRACE("calling.."); - int count = dc_chatlist_get_cnt(dc_chatlist); - //TRACE("result %d", count); - - NAPI_RETURN_INT32(count); -} - -NAPI_METHOD(dcn_chatlist_get_msg_id) { - NAPI_ARGV(2); - NAPI_DC_CHATLIST(); - NAPI_ARGV_INT32(index, 1); - - //TRACE("calling.."); - uint32_t message_id = dc_chatlist_get_msg_id(dc_chatlist, index); - //TRACE("result %d", message_id); - - NAPI_RETURN_UINT32(message_id); -} - -NAPI_METHOD(dcn_chatlist_get_summary) { - NAPI_ARGV(3); - NAPI_DC_CHATLIST(); - NAPI_ARGV_INT32(index, 1); - - //TRACE("calling.."); - dc_chat_t* dc_chat = NULL; - napi_get_value_external(env, argv[2], (void**)&dc_chat); - - dc_lot_t* summary = dc_chatlist_get_summary(dc_chatlist, index, dc_chat); - - napi_value result; - if (summary == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, summary, - finalize_lot, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_chatlist_get_summary2) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(chat_id, 1); - NAPI_ARGV_INT32(message_id, 2); - - //TRACE("calling.."); - dc_lot_t* summary = dc_chatlist_get_summary2(dcn_context->dc_context, chat_id, message_id); - - napi_value result; - if (summary == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, summary, - finalize_lot, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -/** - * dc_contact_t - */ - -NAPI_METHOD(dcn_contact_get_addr) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - char* addr = dc_contact_get_addr(dc_contact); - //TRACE("result %s", addr); - - NAPI_RETURN_AND_UNREF_STRING(addr); -} - -NAPI_METHOD(dcn_contact_get_auth_name) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - char* auth_name = dc_contact_get_auth_name(dc_contact); - - NAPI_RETURN_AND_UNREF_STRING(auth_name); -} - -NAPI_METHOD(dcn_contact_get_color) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - uint32_t color = dc_contact_get_color(dc_contact); - //TRACE("result %d", color); - - NAPI_RETURN_UINT32(color); -} - -NAPI_METHOD(dcn_contact_get_display_name) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - char* display_name = dc_contact_get_display_name(dc_contact); - //TRACE("result %s", display_name); - - NAPI_RETURN_AND_UNREF_STRING(display_name); -} - -NAPI_METHOD(dcn_contact_get_id) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - uint32_t contact_id = dc_contact_get_id(dc_contact); - //TRACE("result %d", contact_id); - - NAPI_RETURN_UINT32(contact_id); -} - -NAPI_METHOD(dcn_contact_get_name) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - char* name = dc_contact_get_name(dc_contact); - //TRACE("result %s", name); - - NAPI_RETURN_AND_UNREF_STRING(name); -} - -NAPI_METHOD(dcn_contact_get_name_n_addr) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - char* name_n_addr = dc_contact_get_name_n_addr(dc_contact); - //TRACE("result %s", name_n_addr); - - NAPI_RETURN_AND_UNREF_STRING(name_n_addr); -} - -NAPI_METHOD(dcn_contact_get_profile_image) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - char* profile_image = dc_contact_get_profile_image(dc_contact); - //TRACE("result %s", profile_image); - - NAPI_RETURN_AND_UNREF_STRING(profile_image); -} - -NAPI_METHOD(dcn_contact_get_status) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - char* status = dc_contact_get_status(dc_contact); - NAPI_RETURN_AND_UNREF_STRING(status); -} - -NAPI_METHOD(dcn_contact_get_last_seen) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - int64_t timestamp = dc_contact_get_last_seen(dc_contact); - NAPI_RETURN_INT64(timestamp); -} - -NAPI_METHOD(dcn_contact_was_seen_recently) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - int seen_recently = dc_contact_was_seen_recently(dc_contact); - NAPI_RETURN_UINT32(seen_recently); -} - -NAPI_METHOD(dcn_contact_is_blocked) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - int is_blocked = dc_contact_is_blocked(dc_contact); - //TRACE("result %d", is_blocked); - - NAPI_RETURN_UINT32(is_blocked); -} - -NAPI_METHOD(dcn_contact_is_verified) { - NAPI_ARGV(1); - NAPI_DC_CONTACT(); - - //TRACE("calling.."); - int is_verified = dc_contact_is_verified(dc_contact); - //TRACE("result %d", is_verified); - - NAPI_RETURN_UINT32(is_verified); -} - -/** - * dc_lot_t - */ - -NAPI_METHOD(dcn_lot_get_id) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - uint32_t id = dc_lot_get_id(dc_lot); - //TRACE("result %d", id); - - NAPI_RETURN_UINT32(id); -} - -NAPI_METHOD(dcn_lot_get_state) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - int state = dc_lot_get_state(dc_lot); - //TRACE("result %d", state); - - NAPI_RETURN_INT32(state); -} - -NAPI_METHOD(dcn_lot_get_text1) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - char* text1 = dc_lot_get_text1(dc_lot); - //TRACE("result %s", text1); - - NAPI_RETURN_AND_UNREF_STRING(text1); -} - -NAPI_METHOD(dcn_lot_get_text1_meaning) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - int text1_meaning = dc_lot_get_text1_meaning(dc_lot); - //TRACE("result %d", text1_meaning); - - NAPI_RETURN_INT32(text1_meaning); -} - -NAPI_METHOD(dcn_lot_get_text2) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - char* text2 = dc_lot_get_text2(dc_lot); - //TRACE("result %s", text2); - - NAPI_RETURN_AND_UNREF_STRING(text2); -} - -NAPI_METHOD(dcn_lot_get_timestamp) { - NAPI_ARGV(1); - NAPI_DC_LOT(); - - //TRACE("calling.."); - int timestamp = dc_lot_get_timestamp(dc_lot); - //TRACE("result %d", timestamp); - - NAPI_RETURN_INT32(timestamp); -} - -/** - * dc_msg_t - */ - -NAPI_METHOD(dcn_msg_get_parent) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - napi_value result; - dc_msg_t* msg = dc_msg_get_parent(dc_msg); - - if (msg == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg, - NULL, &result)); - } - - return result; -} - -NAPI_METHOD(dcn_msg_get_download_state) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - uint32_t download_state = dc_msg_get_download_state(dc_msg); - //TRACE("result %d", download_state); - - NAPI_RETURN_UINT32(download_state); -} - -NAPI_METHOD(dcn_msg_get_chat_id) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - uint32_t chat_id = dc_msg_get_chat_id(dc_msg); - //TRACE("result %d", chat_id); - - NAPI_RETURN_UINT32(chat_id); -} - -NAPI_METHOD(dcn_msg_get_duration) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int duration = dc_msg_get_duration(dc_msg); - //TRACE("result %d", duration); - - NAPI_RETURN_INT32(duration); -} - -NAPI_METHOD(dcn_msg_get_file) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* file = dc_msg_get_file(dc_msg); - //TRACE("result %s", file); - - NAPI_RETURN_AND_UNREF_STRING(file); -} - -NAPI_METHOD(dcn_msg_get_filebytes) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - uint32_t filebytes = dc_msg_get_filebytes(dc_msg); - //TRACE("result %d", filebytes); - - NAPI_RETURN_INT32(filebytes); -} - -NAPI_METHOD(dcn_msg_get_filemime) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* filemime = dc_msg_get_filemime(dc_msg); - //TRACE("result %s", filemime); - - NAPI_RETURN_AND_UNREF_STRING(filemime); -} - -NAPI_METHOD(dcn_msg_get_filename) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* filename = dc_msg_get_filename(dc_msg); - //TRACE("result %s", filename); - - NAPI_RETURN_AND_UNREF_STRING(filename); -} - -NAPI_METHOD(dcn_msg_get_from_id) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - uint32_t contact_id = dc_msg_get_from_id(dc_msg); - //TRACE("result %d", contact_id); - - NAPI_RETURN_UINT32(contact_id); -} - -NAPI_METHOD(dcn_msg_get_height) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int height = dc_msg_get_height(dc_msg); - //TRACE("result %d", height); - - NAPI_RETURN_INT32(height); -} - -NAPI_METHOD(dcn_msg_get_id) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - uint32_t msg_id = dc_msg_get_id(dc_msg); - //TRACE("result %d", msg_id); - - NAPI_RETURN_UINT32(msg_id); -} - -NAPI_METHOD(dcn_msg_get_override_sender_name) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* override_sender_name = dc_msg_get_override_sender_name(dc_msg); - //TRACE("result %s", override_sender_name); - - NAPI_RETURN_AND_UNREF_STRING(override_sender_name); -} - -NAPI_METHOD(dcn_msg_get_quoted_text) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* text = dc_msg_get_quoted_text(dc_msg); - //TRACE("result %s", text); - - NAPI_RETURN_AND_UNREF_STRING(text); -} - -NAPI_METHOD(dcn_msg_get_quoted_msg) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - napi_value result; - dc_msg_t* msg = dc_msg_get_quoted_msg(dc_msg); - - if (msg == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg, - NULL, &result)); - } - - return result; -} - -NAPI_METHOD(dcn_msg_get_received_timestamp) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int timestamp = dc_msg_get_received_timestamp(dc_msg); - //TRACE("result %d", timestamp); - - NAPI_RETURN_INT32(timestamp); -} - - -NAPI_METHOD(dcn_msg_get_setupcodebegin) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* setupcodebegin = dc_msg_get_setupcodebegin(dc_msg); - //TRACE("result %s", setupcodebegin); - - NAPI_RETURN_AND_UNREF_STRING(setupcodebegin); -} - -NAPI_METHOD(dcn_msg_get_showpadlock) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int showpadlock = dc_msg_get_showpadlock(dc_msg); - //TRACE("result %d", showpadlock); - - NAPI_RETURN_INT32(showpadlock); -} - -NAPI_METHOD(dcn_msg_get_sort_timestamp) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int timestamp = dc_msg_get_sort_timestamp(dc_msg); - //TRACE("result %d", timestamp); - - NAPI_RETURN_INT32(timestamp); -} - -NAPI_METHOD(dcn_msg_get_state) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int state = dc_msg_get_state(dc_msg); - //TRACE("result %d", state); - - NAPI_RETURN_INT32(state); -} - -NAPI_METHOD(dcn_msg_get_summary) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - - //TRACE("calling.."); - dc_chat_t* dc_chat = NULL; - napi_get_value_external(env, argv[1], (void**)&dc_chat); - - dc_lot_t* summary = dc_msg_get_summary(dc_msg, dc_chat); - - napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, summary, - finalize_lot, - NULL, &result)); - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_msg_get_summarytext) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_INT32(approx_characters, 1); - - //TRACE("calling.."); - char* summarytext = dc_msg_get_summarytext(dc_msg, approx_characters); - //TRACE("result %s", summarytext); - - NAPI_RETURN_AND_UNREF_STRING(summarytext); -} - -NAPI_METHOD(dcn_msg_get_subject) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* subject = dc_msg_get_subject(dc_msg); - //TRACE("result %s", subject); - - NAPI_RETURN_AND_UNREF_STRING(subject); -} - -NAPI_METHOD(dcn_msg_get_text) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - char* text = dc_msg_get_text(dc_msg); - //TRACE("result %s", text); - - NAPI_RETURN_AND_UNREF_STRING(text); -} - -NAPI_METHOD(dcn_msg_get_timestamp) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int timestamp = dc_msg_get_timestamp(dc_msg); - //TRACE("result %d", timestamp); - - NAPI_RETURN_INT32(timestamp); -} - -NAPI_METHOD(dcn_msg_get_viewtype) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int type = dc_msg_get_viewtype(dc_msg); - //TRACE("result %d", type); - - NAPI_RETURN_INT32(type); -} - -NAPI_METHOD(dcn_msg_get_videochat_type) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - int type = dc_msg_get_videochat_type(dc_msg); - NAPI_RETURN_INT32(type); -} - -NAPI_METHOD(dcn_msg_get_videochat_url) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - char* url = dc_msg_get_videochat_url(dc_msg); - NAPI_RETURN_AND_UNREF_STRING(url); -} - -NAPI_METHOD(dcn_msg_get_width) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int width = dc_msg_get_width(dc_msg); - //TRACE("result %d", width); - - NAPI_RETURN_INT32(width); -} - -NAPI_METHOD(dcn_msg_get_webxdc_info){ - NAPI_ARGV(1); - NAPI_DC_MSG(); - - char* result_json = dc_msg_get_webxdc_info(dc_msg); - - NAPI_RETURN_AND_UNREF_STRING(result_json); -} - -NAPI_METHOD(dcn_msg_has_deviating_timestamp) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int has_deviating_timestamp = dc_msg_has_deviating_timestamp(dc_msg); - //TRACE("result %d", has_deviating_timestamp); - - NAPI_RETURN_INT32(has_deviating_timestamp); -} - -NAPI_METHOD(dcn_msg_has_location) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int has_location = dc_msg_has_location(dc_msg); - //TRACE("result %d", has_location); - - NAPI_RETURN_INT32(has_location); -} - -NAPI_METHOD(dcn_msg_has_html) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int has_html = dc_msg_has_html(dc_msg); - //TRACE("result %d", has_html); - - NAPI_RETURN_INT32(has_html); -} - -NAPI_METHOD(dcn_msg_is_forwarded) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int is_forwarded = dc_msg_is_forwarded(dc_msg); - //TRACE("result %d", is_forwarded); - - NAPI_RETURN_INT32(is_forwarded); -} - -NAPI_METHOD(dcn_msg_is_info) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int is_info = dc_msg_is_info(dc_msg); - //TRACE("result %d", is_info); - - NAPI_RETURN_INT32(is_info); -} - -NAPI_METHOD(dcn_msg_is_sent) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int is_sent = dc_msg_is_sent(dc_msg); - //TRACE("result %d", is_sent); - - NAPI_RETURN_INT32(is_sent); -} - -NAPI_METHOD(dcn_msg_is_setupmessage) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - - //TRACE("calling.."); - int is_setupmessage = dc_msg_is_setupmessage(dc_msg); - //TRACE("result %d", is_setupmessage); - - NAPI_RETURN_INT32(is_setupmessage); -} - -NAPI_METHOD(dcn_msg_latefiling_mediasize) { - NAPI_ARGV(4); - NAPI_DC_MSG(); - NAPI_ARGV_INT32(width, 1); - NAPI_ARGV_INT32(height, 2); - NAPI_ARGV_INT32(duration, 3); - - //TRACE("calling.."); - dc_msg_latefiling_mediasize(dc_msg, width, height, duration); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - - -NAPI_METHOD(dcn_msg_force_plaintext) { - NAPI_ARGV(1); - NAPI_DC_MSG(); - dc_msg_force_plaintext(dc_msg); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_dimension) { - NAPI_ARGV(3); - NAPI_DC_MSG(); - NAPI_ARGV_INT32(width, 1); - NAPI_ARGV_INT32(height, 2); - - //TRACE("calling.."); - dc_msg_set_dimension(dc_msg, width, height); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_duration) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_INT32(duration, 1); - - //TRACE("calling.."); - dc_msg_set_duration(dc_msg, duration); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_override_sender_name) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_UTF8_MALLOC(override_sender_name, 1); - - //TRACE("calling.."); - dc_msg_set_override_sender_name(dc_msg, override_sender_name); - //TRACE("done"); - - free(override_sender_name); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_file) { - NAPI_ARGV(3); - NAPI_DC_MSG(); - NAPI_ARGV_UTF8_MALLOC(file, 1); - NAPI_ARGV_UTF8_MALLOC(filemime, 2); - - //TRACE("calling.."); - dc_msg_set_file(dc_msg, file, filemime && filemime[0] ? filemime : NULL); - //TRACE("done"); - - free(file); - free(filemime); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_html) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_UTF8_MALLOC(html, 1); - - //TRACE("calling.."); - dc_msg_set_html(dc_msg, html); - //TRACE("done"); - - free(html); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_quote) { - NAPI_ARGV(2); - NAPI_ARGV_DC_MSG(dc_msg, 0) - - dc_msg_t* dc_msg_quote = NULL; - napi_get_value_external(env, argv[1], (void**)&dc_msg_quote); - - dc_msg_set_quote(dc_msg, dc_msg_quote); - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_msg_set_text) { - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_UTF8_MALLOC(text, 1); - - //TRACE("calling.."); - dc_msg_set_text(dc_msg, text); - //TRACE("done"); - - free(text); - - NAPI_RETURN_UNDEFINED(); -} - -/** - * locations - */ - -NAPI_METHOD(dcn_msg_set_location) { - NAPI_ARGV(3); - NAPI_DC_MSG(); - NAPI_ARGV_DOUBLE(latitude, 1); - NAPI_ARGV_DOUBLE(longitude, 2); - - //TRACE("calling.."); - dc_msg_set_location(dc_msg, latitude, longitude); - //TRACE("done"); - - NAPI_RETURN_UNDEFINED(); -} - -/** - * locations - */ - -NAPI_METHOD(dcn_set_location) { - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_DOUBLE(latitude, 1); - NAPI_ARGV_DOUBLE(longitude, 2); - NAPI_ARGV_DOUBLE(accuracy, 3); - - //TRACE("calling.."); - int result = dc_set_location(dcn_context->dc_context, latitude, longitude, accuracy); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_get_locations) { - NAPI_ARGV(5); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_INT32(chat_id, 1); - NAPI_ARGV_INT32(contact_id, 2); - NAPI_ARGV_INT32(timestamp_from, 3); - NAPI_ARGV_INT32(timestamp_to, 4); - - //TRACE("calling.."); - dc_array_t* locations = dc_get_locations(dcn_context->dc_context, - chat_id, - contact_id, - timestamp_from, - timestamp_to); - - napi_value napi_locations; - NAPI_STATUS_THROWS(napi_create_external(env, locations, - finalize_array, - NULL, &napi_locations)); - //TRACE("done"); - - return napi_locations; -} - -NAPI_METHOD(dcn_array_get_cnt) { - NAPI_ARGV(1); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t size = dc_array_get_cnt(dc_array); - - napi_value napi_size; - NAPI_STATUS_THROWS(napi_create_uint32(env, size, &napi_size)); - //TRACE("done"); - - return napi_size; -} - -NAPI_METHOD(dcn_array_get_id) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - uint32_t id = dc_array_get_id(dc_array, index); - - napi_value napi_id; - NAPI_STATUS_THROWS(napi_create_uint32(env, id, &napi_id)); - //TRACE("done"); - - return napi_id; -} - -NAPI_METHOD(dcn_array_get_accuracy) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - double accuracy = dc_array_get_accuracy(dc_array, index); - - napi_value napi_accuracy; - NAPI_STATUS_THROWS(napi_create_double(env, accuracy, &napi_accuracy)); - //TRACE("done"); - - return napi_accuracy; -} - -NAPI_METHOD(dcn_array_get_longitude) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - double longitude = dc_array_get_longitude(dc_array, index); - - napi_value napi_longitude; - NAPI_STATUS_THROWS(napi_create_double(env, longitude, &napi_longitude)); - //TRACE("done"); - - return napi_longitude; -} - -NAPI_METHOD(dcn_array_get_latitude) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - double latitude = dc_array_get_latitude(dc_array, index); - - napi_value napi_latitude; - NAPI_STATUS_THROWS(napi_create_double(env, latitude, &napi_latitude)); - //TRACE("done"); - - return napi_latitude; -} - -NAPI_METHOD(dcn_array_get_timestamp) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - int timestamp = dc_array_get_timestamp(dc_array, index); - - napi_value napi_timestamp; - NAPI_STATUS_THROWS(napi_create_int64(env, timestamp, &napi_timestamp)); - //TRACE("done"); - - return napi_timestamp; -} - -NAPI_METHOD(dcn_array_get_msg_id) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - uint32_t msg_id = dc_array_get_msg_id(dc_array, index); - - napi_value napi_msg_id; - NAPI_STATUS_THROWS(napi_create_uint32(env, msg_id, &napi_msg_id)); - //TRACE("done"); - - return napi_msg_id; -} - -NAPI_METHOD(dcn_array_is_independent) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - int result = dc_array_is_independent(dc_array, index); - //TRACE("result %d", result); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_array_get_marker) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - char* marker = dc_array_get_marker(dc_array, index); - //TRACE("result %s", marker); - - NAPI_RETURN_AND_UNREF_STRING(marker); -} - -NAPI_METHOD(dcn_array_get_contact_id) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - uint32_t contact_id = dc_array_get_contact_id(dc_array, index); - - napi_value napi_contact_id; - NAPI_STATUS_THROWS(napi_create_uint32(env, contact_id, &napi_contact_id)); - //TRACE("done"); - - return napi_contact_id; -} - -NAPI_METHOD(dcn_array_get_chat_id) { - NAPI_ARGV(2); - NAPI_DC_ARRAY(); - - //TRACE("calling.."); - uint32_t index; - NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index)); - - uint32_t chat_id = dc_array_get_chat_id(dc_array, index); - - napi_value napi_chat_id; - NAPI_STATUS_THROWS(napi_create_uint32(env, chat_id, &napi_chat_id)); - //TRACE("done"); - - return napi_chat_id; -} - -NAPI_METHOD(dcn_provider_new_from_email) { - NAPI_ARGV(2); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UTF8_MALLOC(email, 1) - - //TRACE("calling.."); - napi_value result; - dc_provider_t* provider = dc_provider_new_from_email(dcn_context->dc_context, email); - - if (provider == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - NAPI_STATUS_THROWS(napi_create_external(env, provider, finalize_provider, - NULL, &result)); - } - //TRACE("done"); - - return result; -} - -NAPI_METHOD(dcn_provider_get_overview_page) { - NAPI_ARGV(1); - NAPI_DC_PROVIDER(); - - //TRACE("calling.."); - char* overview_page = dc_provider_get_overview_page(dc_provider); - //TRACE("result %s", overview_page); - - NAPI_RETURN_AND_UNREF_STRING(overview_page); -} - -NAPI_METHOD(dcn_provider_get_before_login_hint) { - NAPI_ARGV(1); - NAPI_DC_PROVIDER(); - - //TRACE("calling.."); - char* before_login_hint = dc_provider_get_before_login_hint(dc_provider); - //TRACE("result %s", before_login_hint); - - NAPI_RETURN_AND_UNREF_STRING(before_login_hint); -} - -NAPI_METHOD(dcn_provider_get_status) { - NAPI_ARGV(1); - NAPI_DC_PROVIDER(); - - //TRACE("calling.."); - int status = dc_provider_get_status(dc_provider); - //TRACE("result %s", status); - - NAPI_RETURN_INT32(status) -} - -// webxdc - -NAPI_METHOD(dcn_send_webxdc_status_update){ - NAPI_ARGV(4); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - NAPI_ARGV_UTF8_MALLOC(json, 2); - NAPI_ARGV_UTF8_MALLOC(descr, 3); - - int result = dc_send_webxdc_status_update(dcn_context->dc_context, msg_id, json, descr); - free(json); - free(descr); - - NAPI_RETURN_UINT32(result); -} - -NAPI_METHOD(dcn_get_webxdc_status_updates){ - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(msg_id, 1); - NAPI_ARGV_UINT32(serial, 2); - - char* result_json = dc_get_webxdc_status_updates(dcn_context->dc_context, msg_id, serial); - - NAPI_RETURN_AND_UNREF_STRING(result_json); -} - -NAPI_METHOD(dcn_msg_get_webxdc_blob){ - NAPI_ARGV(2); - NAPI_DC_MSG(); - NAPI_ARGV_UTF8_MALLOC(filename, 1); - - size_t size; - char* data = dc_msg_get_webxdc_blob(dc_msg, filename, &size); - free(filename); - - napi_value jsbuffer; - if (data == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &jsbuffer)); - } else { - // https://nodejs.org/api/n-api.html#napi_create_buffer_copy - NAPI_STATUS_THROWS(napi_create_buffer_copy(env, - size, - data, - NULL, - &jsbuffer)) - dc_str_unref(data); - } - - return jsbuffer; -} - - -// dc_accounts_* - -NAPI_METHOD(dcn_accounts_new) { - NAPI_ARGV(2); - NAPI_ARGV_UTF8_MALLOC(dir, 0); - NAPI_ARGV_INT32(writable, 1); - TRACE("calling.."); - - dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t)); - if (dcn_accounts == NULL) { - napi_throw_error(env, NULL, "dcn_accounts is null"); \ - } - - - dcn_accounts->dc_accounts = dc_accounts_new(dir, writable); - - napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts, - NULL, NULL, &result)); - return result; -} - - -NAPI_METHOD(dcn_accounts_unref) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - - TRACE("Unrefing dc_accounts"); - dcn_accounts->gc = 1; - if (dcn_accounts->event_handler_thread != 0) { - dc_accounts_stop_io(dcn_accounts->dc_accounts); - uv_thread_join(&dcn_accounts->event_handler_thread); - dcn_accounts->event_handler_thread = 0; - } - if (dcn_accounts->jsonrpc_instance) { - dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}"); - uv_thread_join(&dcn_accounts->jsonrpc_thread); - dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance); - dcn_accounts->jsonrpc_instance = NULL; - } - dc_accounts_unref(dcn_accounts->dc_accounts); - dcn_accounts->dc_accounts = NULL; - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_accounts_add_account) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - int account_id = dc_accounts_add_account(dcn_accounts->dc_accounts); - - NAPI_RETURN_UINT32(account_id); -} - -NAPI_METHOD(dcn_accounts_add_closed_account) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - int account_id = dc_accounts_add_closed_account(dcn_accounts->dc_accounts); - - NAPI_RETURN_UINT32(account_id); -} - - -NAPI_METHOD(dcn_accounts_migrate_account) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - NAPI_ARGV_UTF8_MALLOC(dbfile, 1); - - uint32_t account_id = dc_accounts_migrate_account(dcn_accounts->dc_accounts, dbfile); - - NAPI_RETURN_UINT32(account_id); -} - -NAPI_METHOD(dcn_accounts_remove_account) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - NAPI_ARGV_UINT32(account_id, 1); - - int result = dc_accounts_remove_account(dcn_accounts->dc_accounts, account_id); - - NAPI_RETURN_INT32(result); -} - -NAPI_METHOD(dcn_accounts_get_all) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - dc_array_t* accounts = dc_accounts_get_all(dcn_accounts->dc_accounts); - napi_value js_array = dc_array_to_js_array(env, accounts); - dc_array_unref(accounts); - - return js_array; -} - -NAPI_METHOD(dcn_accounts_get_account) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - NAPI_ARGV_UINT32(account_id, 1); - - dc_context_t* account_context = dc_accounts_get_account(dcn_accounts->dc_accounts, account_id); - - - napi_value result; - if (account_context == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t)); - dcn_context->dc_context = account_context; - - NAPI_STATUS_THROWS(napi_create_external(env, dcn_context, - NULL, NULL, &result)); - } - - return result; -} - -NAPI_METHOD(dcn_accounts_get_selected_account) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - dc_context_t* account_context = dc_accounts_get_selected_account(dcn_accounts->dc_accounts); - - - napi_value result; - if (account_context == NULL) { - NAPI_STATUS_THROWS(napi_get_null(env, &result)); - } else { - dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t)); - dcn_context->dc_context = account_context; - - NAPI_STATUS_THROWS(napi_create_external(env, dcn_context, - NULL, NULL, &result)); - } - - return result; -} - -NAPI_METHOD(dcn_accounts_select_account) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - NAPI_ARGV_UINT32(account_id, 1); - - int result = dc_accounts_select_account(dcn_accounts->dc_accounts, account_id); - NAPI_RETURN_UINT32(result); -} - -NAPI_METHOD(dcn_accounts_start_io) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - TRACE("calling..."); - dc_accounts_start_io(dcn_accounts->dc_accounts); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_accounts_stop_io) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - dc_accounts_stop_io(dcn_accounts->dc_accounts); - - NAPI_RETURN_UNDEFINED(); -} - - -NAPI_METHOD(dcn_accounts_maybe_network) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - dc_accounts_maybe_network(dcn_accounts->dc_accounts); - - NAPI_RETURN_UNDEFINED(); -} - - -NAPI_METHOD(dcn_accounts_maybe_network_lost) { - NAPI_ARGV(1); - NAPI_DCN_ACCOUNTS(); - - dc_accounts_maybe_network_lost(dcn_accounts->dc_accounts); - - NAPI_RETURN_UNDEFINED(); -} - -static void accounts_event_handler_thread_func(void* arg) -{ - dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg; - - TRACE("event_handler_thread_func starting"); - - dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts); - dc_event_t* event; - while (true) { - if (dc_event_emitter == NULL) { - TRACE("event emitter is null, bailing"); - break; - } - event = dc_get_next_event(dc_event_emitter); - if (event == NULL) { - TRACE("no more events"); - break; - } - - if (!dcn_accounts->threadsafe_event_handler) { - TRACE("threadsafe_event_handler not set, bailing"); - break; - } - - // Don't process events if we're being garbage collected! - if (dcn_accounts->gc == 1) { - TRACE("dc_accounts has been destroyed, bailing"); - break; - } - - - napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_event_handler, event, napi_tsfn_blocking); - - if (status == napi_closing) { - TRACE("JS function got released, bailing"); - break; - } - } - - dc_event_emitter_unref(dc_event_emitter); - - TRACE("event_handler_thread_func ended"); - - napi_release_threadsafe_function(dcn_accounts->threadsafe_event_handler, napi_tsfn_release); -} - -static void call_accounts_js_event_handler(napi_env env, napi_value js_callback, void* _context, void* data) -{ - dc_event_t* dc_event = (dc_event_t*)data; - - napi_value global; - napi_status status = napi_get_global(env, &global); - - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to get global"); - } - - -#define CALL_JS_CALLBACK_ACCOUNTS_ARGC 4 - - const int argc = CALL_JS_CALLBACK_ACCOUNTS_ARGC; - napi_value argv[CALL_JS_CALLBACK_ACCOUNTS_ARGC]; - - const int event_id = dc_event_get_id(dc_event); - - status = napi_create_uint32(env, event_id, &argv[0]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[0] for event_handler arguments"); - } - - const int account_id = dc_event_get_account_id(dc_event); - status = napi_create_uint32(env, account_id, &argv[1]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[1] for event_handler arguments"); - } - - - status = napi_create_int32(env, dc_event_get_data1_int(dc_event), &argv[2]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments"); - } - - if DC_EVENT_DATA2_IS_STRING(event_id) { - char* data2_string = dc_event_get_data2_str(dc_event); - // Quick fix for https://github.com/deltachat/deltachat-core-rust/issues/1949 - if (data2_string != 0) { - status = napi_create_string_utf8(env, data2_string, NAPI_AUTO_LENGTH, &argv[3]); - } else { - status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[3]); - } - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments"); - } - dc_str_unref(data2_string); - } else { - status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[3]); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments"); - } - } - - dc_event_unref(dc_event); - dc_event = NULL; - - TRACE("calling back into js"); - - napi_value result; - status = napi_call_function( - env, - global, - js_callback, - argc, - argv, - &result); - - if (status != napi_ok) { - TRACE("Unable to call event_handler callback2"); - const napi_extended_error_info* error_result; - NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result)); - } -} - -NAPI_METHOD(dcn_accounts_start_event_handler) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - napi_value callback = argv[1]; - - TRACE("calling.."); - napi_value async_resource_name; - NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_event_callback", NAPI_AUTO_LENGTH, &async_resource_name)); - - TRACE("creating threadsafe function.."); - - NAPI_STATUS_THROWS(napi_create_threadsafe_function( - env, - callback, - 0, - async_resource_name, - 1000, // max_queue_size - 1, - NULL, - NULL, - dcn_accounts, - call_accounts_js_event_handler, - &dcn_accounts->threadsafe_event_handler)); - TRACE("done"); - - dcn_accounts->gc = 0; - TRACE("creating uv thread.."); - uv_thread_create(&dcn_accounts->event_handler_thread, accounts_event_handler_thread_func, dcn_accounts); - - NAPI_RETURN_UNDEFINED(); -} - -// JSON RPC - -static void accounts_jsonrpc_thread_func(void* arg) -{ - dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg; - TRACE("accounts_jsonrpc_thread_func starting"); - char* response; - while (true) { - response = dc_jsonrpc_next_response(dcn_accounts->jsonrpc_instance); - if (response == NULL) { - // done or broken - break; - } - - if (!dcn_accounts->threadsafe_jsonrpc_handler) { - TRACE("threadsafe_jsonrpc_handler not set, bailing"); - break; - } - // Don't process events if we're being garbage collected! - if (dcn_accounts->gc == 1) { - TRACE("dc_accounts has been destroyed, bailing"); - break; - } - - napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking); - - if (status == napi_closing) { - TRACE("JS function got released, bailing"); - break; - } - } - TRACE("accounts_jsonrpc_thread_func ended"); - napi_release_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, napi_tsfn_release); -} - -static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callback, void* _context, void* data) -{ - char* response = (char*)data; - napi_value global; - napi_status status = napi_get_global(env, &global); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to get global"); - } - - napi_value argv[1]; - if (response != 0) { - status = napi_create_string_utf8(env, response, NAPI_AUTO_LENGTH, &argv[0]); - } else { - status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[0]); - } - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments"); - } - dc_str_unref(response); - - TRACE("calling back into js"); - napi_value result; - status = napi_call_function( - env, - global, - js_callback, - 1, - argv, - &result); - if (status != napi_ok) { - TRACE("Unable to call jsonrpc_handler callback2"); - const napi_extended_error_info* error_result; - NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result)); - } -} - -NAPI_METHOD(dcn_accounts_start_jsonrpc) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - napi_value callback = argv[1]; - - TRACE("calling.."); - napi_value async_resource_name; - NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name)); - - TRACE("creating threadsafe function.."); - - NAPI_STATUS_THROWS(napi_create_threadsafe_function( - env, - callback, - 0, - async_resource_name, - 1000, // max_queue_size - 1, - NULL, - NULL, - NULL, - call_accounts_js_jsonrpc_handler, - &dcn_accounts->threadsafe_jsonrpc_handler)); - TRACE("done"); - - dcn_accounts->gc = 0; - dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts); - - TRACE("creating uv thread.."); - uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts); - - NAPI_RETURN_UNDEFINED(); -} - -NAPI_METHOD(dcn_json_rpc_request) { - NAPI_ARGV(2); - NAPI_DCN_ACCOUNTS(); - if (!dcn_accounts->jsonrpc_instance) { - const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?"; - NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); - } - NAPI_ARGV_UTF8_MALLOC(request, 1); - dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request); - free(request); - NAPI_RETURN_UNDEFINED(); -} - - -NAPI_INIT() { - /** - * Accounts - */ - - NAPI_EXPORT_FUNCTION(dcn_accounts_new); - NAPI_EXPORT_FUNCTION(dcn_accounts_unref); - NAPI_EXPORT_FUNCTION(dcn_accounts_add_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_add_closed_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_migrate_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_remove_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_get_all); - NAPI_EXPORT_FUNCTION(dcn_accounts_get_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_select_account); - NAPI_EXPORT_FUNCTION(dcn_accounts_start_io); - NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io); - NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network); - NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network_lost); - - NAPI_EXPORT_FUNCTION(dcn_accounts_start_event_handler); - - - /** - * Main context - */ - - NAPI_EXPORT_FUNCTION(dcn_context_new); - NAPI_EXPORT_FUNCTION(dcn_context_new_closed); - NAPI_EXPORT_FUNCTION(dcn_context_open); - NAPI_EXPORT_FUNCTION(dcn_context_is_open); - NAPI_EXPORT_FUNCTION(dcn_context_unref); - NAPI_EXPORT_FUNCTION(dcn_start_event_handler); - - /** - * Static functions - */ - - NAPI_EXPORT_FUNCTION(dcn_maybe_valid_addr); - - /** - * dcn_context_t - */ - - NAPI_EXPORT_FUNCTION(dcn_add_address_book); - NAPI_EXPORT_FUNCTION(dcn_add_contact_to_chat); - NAPI_EXPORT_FUNCTION(dcn_add_device_msg); - NAPI_EXPORT_FUNCTION(dcn_block_contact); - NAPI_EXPORT_FUNCTION(dcn_check_qr); - NAPI_EXPORT_FUNCTION(dcn_configure); - NAPI_EXPORT_FUNCTION(dcn_continue_key_transfer); - NAPI_EXPORT_FUNCTION(dcn_create_chat_by_contact_id); - NAPI_EXPORT_FUNCTION(dcn_create_broadcast_list); - NAPI_EXPORT_FUNCTION(dcn_create_contact); - NAPI_EXPORT_FUNCTION(dcn_create_group_chat); - NAPI_EXPORT_FUNCTION(dcn_delete_chat); - NAPI_EXPORT_FUNCTION(dcn_delete_contact); - NAPI_EXPORT_FUNCTION(dcn_delete_msgs); - NAPI_EXPORT_FUNCTION(dcn_forward_msgs); - NAPI_EXPORT_FUNCTION(dcn_get_blobdir); - NAPI_EXPORT_FUNCTION(dcn_get_blocked_cnt); - NAPI_EXPORT_FUNCTION(dcn_get_blocked_contacts); - NAPI_EXPORT_FUNCTION(dcn_get_chat); - NAPI_EXPORT_FUNCTION(dcn_get_chat_contacts); - NAPI_EXPORT_FUNCTION(dcn_get_chat_encrinfo); - NAPI_EXPORT_FUNCTION(dcn_get_chat_id_by_contact_id); - NAPI_EXPORT_FUNCTION(dcn_get_chat_media); - NAPI_EXPORT_FUNCTION(dcn_get_mime_headers); - NAPI_EXPORT_FUNCTION(dcn_get_chat_msgs); - NAPI_EXPORT_FUNCTION(dcn_get_chatlist); - NAPI_EXPORT_FUNCTION(dcn_get_config); - NAPI_EXPORT_FUNCTION(dcn_get_contact); - NAPI_EXPORT_FUNCTION(dcn_get_contact_encrinfo); - NAPI_EXPORT_FUNCTION(dcn_get_contacts); - NAPI_EXPORT_FUNCTION(dcn_get_connectivity); - NAPI_EXPORT_FUNCTION(dcn_get_connectivity_html); - NAPI_EXPORT_FUNCTION(dcn_was_device_msg_ever_added); - NAPI_EXPORT_FUNCTION(dcn_get_draft); - NAPI_EXPORT_FUNCTION(dcn_get_fresh_msg_cnt); - NAPI_EXPORT_FUNCTION(dcn_get_fresh_msgs); - NAPI_EXPORT_FUNCTION(dcn_get_info); - NAPI_EXPORT_FUNCTION(dcn_get_msg); - NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt); - NAPI_EXPORT_FUNCTION(dcn_get_msg_info); - NAPI_EXPORT_FUNCTION(dcn_get_msg_html); - NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility); - NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr); - NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg); - NAPI_EXPORT_FUNCTION(dcn_imex); - NAPI_EXPORT_FUNCTION(dcn_imex_has_backup); - NAPI_EXPORT_FUNCTION(dcn_initiate_key_transfer); - NAPI_EXPORT_FUNCTION(dcn_is_configured); - NAPI_EXPORT_FUNCTION(dcn_is_contact_in_chat); - - - - NAPI_EXPORT_FUNCTION(dcn_accept_chat); - NAPI_EXPORT_FUNCTION(dcn_block_chat); - NAPI_EXPORT_FUNCTION(dcn_join_securejoin); - NAPI_EXPORT_FUNCTION(dcn_lookup_contact_id_by_addr); - NAPI_EXPORT_FUNCTION(dcn_marknoticed_chat); - NAPI_EXPORT_FUNCTION(dcn_download_full_msg); - NAPI_EXPORT_FUNCTION(dcn_markseen_msgs); - NAPI_EXPORT_FUNCTION(dcn_maybe_network); - NAPI_EXPORT_FUNCTION(dcn_msg_new); - NAPI_EXPORT_FUNCTION(dcn_remove_contact_from_chat); - NAPI_EXPORT_FUNCTION(dcn_search_msgs); - NAPI_EXPORT_FUNCTION(dcn_send_msg); - NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation); - NAPI_EXPORT_FUNCTION(dcn_set_chat_name); - NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer); - NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer); - NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image); - NAPI_EXPORT_FUNCTION(dcn_set_chat_mute_duration); - NAPI_EXPORT_FUNCTION(dcn_set_config); - NAPI_EXPORT_FUNCTION(dcn_set_config_null); - NAPI_EXPORT_FUNCTION(dcn_set_config_from_qr); - NAPI_EXPORT_FUNCTION(dcn_estimate_deletion_cnt); - NAPI_EXPORT_FUNCTION(dcn_set_draft); - NAPI_EXPORT_FUNCTION(dcn_set_stock_translation); - NAPI_EXPORT_FUNCTION(dcn_start_io); - NAPI_EXPORT_FUNCTION(dcn_stop_io); - NAPI_EXPORT_FUNCTION(dcn_stop_ongoing_process); - - /** - * dc_chat_t - */ - - NAPI_EXPORT_FUNCTION(dcn_chat_get_color); - NAPI_EXPORT_FUNCTION(dcn_chat_get_visibility); - NAPI_EXPORT_FUNCTION(dcn_chat_get_id); - NAPI_EXPORT_FUNCTION(dcn_chat_get_name); - NAPI_EXPORT_FUNCTION(dcn_chat_get_mailinglist_addr); - NAPI_EXPORT_FUNCTION(dcn_chat_get_profile_image); - NAPI_EXPORT_FUNCTION(dcn_chat_get_type); - NAPI_EXPORT_FUNCTION(dcn_chat_is_self_talk); - NAPI_EXPORT_FUNCTION(dcn_chat_is_unpromoted); - NAPI_EXPORT_FUNCTION(dcn_chat_can_send); - NAPI_EXPORT_FUNCTION(dcn_chat_is_protected); - NAPI_EXPORT_FUNCTION(dcn_chat_is_device_talk); - NAPI_EXPORT_FUNCTION(dcn_chat_is_muted); - NAPI_EXPORT_FUNCTION(dcn_chat_is_contact_request); - - /** - * dc_chatlist_t - */ - - NAPI_EXPORT_FUNCTION(dcn_chatlist_get_chat_id); - NAPI_EXPORT_FUNCTION(dcn_chatlist_get_cnt); - NAPI_EXPORT_FUNCTION(dcn_chatlist_get_msg_id); - NAPI_EXPORT_FUNCTION(dcn_chatlist_get_summary); - NAPI_EXPORT_FUNCTION(dcn_chatlist_get_summary2); - - /** - * dc_contact_t - */ - - NAPI_EXPORT_FUNCTION(dcn_contact_get_addr); - NAPI_EXPORT_FUNCTION(dcn_contact_get_auth_name); - NAPI_EXPORT_FUNCTION(dcn_contact_get_color); - NAPI_EXPORT_FUNCTION(dcn_contact_get_display_name); - NAPI_EXPORT_FUNCTION(dcn_contact_get_id); - NAPI_EXPORT_FUNCTION(dcn_contact_get_name); - NAPI_EXPORT_FUNCTION(dcn_contact_get_name_n_addr); - NAPI_EXPORT_FUNCTION(dcn_contact_get_profile_image); - NAPI_EXPORT_FUNCTION(dcn_contact_get_status); - NAPI_EXPORT_FUNCTION(dcn_contact_get_last_seen); - NAPI_EXPORT_FUNCTION(dcn_contact_is_blocked); - NAPI_EXPORT_FUNCTION(dcn_contact_is_verified); - - /** - * dc_lot_t - */ - - NAPI_EXPORT_FUNCTION(dcn_lot_get_id); - NAPI_EXPORT_FUNCTION(dcn_lot_get_state); - NAPI_EXPORT_FUNCTION(dcn_lot_get_text1); - NAPI_EXPORT_FUNCTION(dcn_lot_get_text1_meaning); - NAPI_EXPORT_FUNCTION(dcn_lot_get_text2); - NAPI_EXPORT_FUNCTION(dcn_lot_get_timestamp); - - /** - * dc_msg_t - */ - - NAPI_EXPORT_FUNCTION(dcn_msg_get_parent); - NAPI_EXPORT_FUNCTION(dcn_msg_get_download_state); - NAPI_EXPORT_FUNCTION(dcn_msg_get_chat_id); - NAPI_EXPORT_FUNCTION(dcn_msg_get_duration); - NAPI_EXPORT_FUNCTION(dcn_msg_get_file); - NAPI_EXPORT_FUNCTION(dcn_msg_get_filebytes); - NAPI_EXPORT_FUNCTION(dcn_msg_get_filemime); - NAPI_EXPORT_FUNCTION(dcn_msg_get_filename); - NAPI_EXPORT_FUNCTION(dcn_msg_get_from_id); - NAPI_EXPORT_FUNCTION(dcn_msg_get_height); - NAPI_EXPORT_FUNCTION(dcn_msg_get_id); - NAPI_EXPORT_FUNCTION(dcn_msg_get_override_sender_name); - NAPI_EXPORT_FUNCTION(dcn_msg_get_quoted_text); - NAPI_EXPORT_FUNCTION(dcn_msg_get_quoted_msg); - NAPI_EXPORT_FUNCTION(dcn_msg_get_received_timestamp); - NAPI_EXPORT_FUNCTION(dcn_msg_get_setupcodebegin); - NAPI_EXPORT_FUNCTION(dcn_msg_get_showpadlock); - NAPI_EXPORT_FUNCTION(dcn_msg_get_sort_timestamp); - NAPI_EXPORT_FUNCTION(dcn_msg_get_state); - NAPI_EXPORT_FUNCTION(dcn_msg_get_summary); - NAPI_EXPORT_FUNCTION(dcn_msg_get_summarytext); - NAPI_EXPORT_FUNCTION(dcn_msg_get_subject); - NAPI_EXPORT_FUNCTION(dcn_msg_get_text); - NAPI_EXPORT_FUNCTION(dcn_msg_get_timestamp); - NAPI_EXPORT_FUNCTION(dcn_msg_get_viewtype); - NAPI_EXPORT_FUNCTION(dcn_msg_get_videochat_type); - NAPI_EXPORT_FUNCTION(dcn_msg_get_videochat_url); - NAPI_EXPORT_FUNCTION(dcn_msg_get_width); - NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_info); - NAPI_EXPORT_FUNCTION(dcn_msg_has_deviating_timestamp); - NAPI_EXPORT_FUNCTION(dcn_msg_has_location); - NAPI_EXPORT_FUNCTION(dcn_msg_has_html); - NAPI_EXPORT_FUNCTION(dcn_msg_is_forwarded); - NAPI_EXPORT_FUNCTION(dcn_msg_is_info); - NAPI_EXPORT_FUNCTION(dcn_msg_is_sent); - NAPI_EXPORT_FUNCTION(dcn_msg_is_setupmessage); - NAPI_EXPORT_FUNCTION(dcn_msg_latefiling_mediasize); - NAPI_EXPORT_FUNCTION(dcn_msg_force_plaintext); - NAPI_EXPORT_FUNCTION(dcn_msg_set_dimension); - NAPI_EXPORT_FUNCTION(dcn_msg_set_duration); - NAPI_EXPORT_FUNCTION(dcn_msg_set_override_sender_name); - NAPI_EXPORT_FUNCTION(dcn_msg_set_file); - NAPI_EXPORT_FUNCTION(dcn_msg_set_html); - NAPI_EXPORT_FUNCTION(dcn_msg_set_quote); - NAPI_EXPORT_FUNCTION(dcn_msg_set_text); - NAPI_EXPORT_FUNCTION(dcn_msg_set_location); - - /** - * dc_location - */ - NAPI_EXPORT_FUNCTION(dcn_set_location); - NAPI_EXPORT_FUNCTION(dcn_get_locations); - - /** - * dc_provider - */ - NAPI_EXPORT_FUNCTION(dcn_provider_new_from_email); - NAPI_EXPORT_FUNCTION(dcn_provider_get_overview_page); - NAPI_EXPORT_FUNCTION(dcn_provider_get_before_login_hint); - NAPI_EXPORT_FUNCTION(dcn_provider_get_status); - - /** - * dc_array - */ - NAPI_EXPORT_FUNCTION(dcn_array_get_cnt); - NAPI_EXPORT_FUNCTION(dcn_array_get_id); - NAPI_EXPORT_FUNCTION(dcn_array_get_accuracy); - NAPI_EXPORT_FUNCTION(dcn_array_get_latitude); - NAPI_EXPORT_FUNCTION(dcn_array_get_longitude); - NAPI_EXPORT_FUNCTION(dcn_array_get_timestamp); - NAPI_EXPORT_FUNCTION(dcn_array_get_msg_id); - NAPI_EXPORT_FUNCTION(dcn_array_is_independent); - NAPI_EXPORT_FUNCTION(dcn_array_get_contact_id); - NAPI_EXPORT_FUNCTION(dcn_array_get_chat_id); - NAPI_EXPORT_FUNCTION(dcn_array_get_marker); - - /** webxdc **/ - - NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update); - NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates); - NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob); - - - /** jsonrpc **/ - NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc); - NAPI_EXPORT_FUNCTION(dcn_json_rpc_request); -} diff --git a/node/src/napi-macros-extensions.h b/node/src/napi-macros-extensions.h deleted file mode 100644 index 968bc02ca9..0000000000 --- a/node/src/napi-macros-extensions.h +++ /dev/null @@ -1,144 +0,0 @@ -#include - -#undef NAPI_STATUS_THROWS - -#define NAPI_STATUS_THROWS(call) \ - if ((call) != napi_ok) { \ - napi_throw_error(env, NULL, #call " failed!"); \ - } - -#define NAPI_DCN_CONTEXT() \ - dcn_context_t* dcn_context; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_context)); \ - if (!dcn_context) { \ - const char* msg = "Provided dnc_context is null"; \ - NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \ - } \ - if (!dcn_context->dc_context) { \ - const char* msg = "Provided dc_context is null, did you close the context or not open it?"; \ - NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \ - } - -#define NAPI_DCN_ACCOUNTS() \ - dcn_accounts_t* dcn_accounts; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \ - if (!dcn_accounts) { \ - const char* msg = "Provided dcn_acounts is null"; \ - NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \ - } \ - if (!dcn_accounts->dc_accounts) { \ - const char* msg = "Provided dc_accounts is null, did you unref the accounts object?"; \ - NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \ - } - - -#define NAPI_DC_CHAT() \ - dc_chat_t* dc_chat; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chat)); - -#define NAPI_DC_CHATLIST() \ - dc_chatlist_t* dc_chatlist; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chatlist)); - -#define NAPI_DC_CONTACT() \ - dc_contact_t* dc_contact; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_contact)); - -#define NAPI_DC_LOT() \ - dc_lot_t* dc_lot; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_lot)); - -#define NAPI_DC_MSG() \ - dc_msg_t* dc_msg; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_msg)); - -#define NAPI_ARGV_DC_MSG(name, position) \ - dc_msg_t* name; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[position], (void**)&name)); - -#define NAPI_DC_PROVIDER() \ - dc_provider_t* dc_provider; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_provider)); - -#define NAPI_DC_ARRAY() \ - dc_array_t* dc_array; \ - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_array)); - -#define NAPI_RETURN_UNDEFINED() \ - return 0; - -#define NAPI_RETURN_UINT64(name) \ - napi_value return_int64; \ - NAPI_STATUS_THROWS(napi_create_bigint_int64(env, name, &return_int64)); \ - return return_int64; - -#define NAPI_RETURN_INT64(name) \ - napi_value return_int64; \ - NAPI_STATUS_THROWS(napi_create_int64(env, name, &return_int64)); \ - return return_int64; - - -#define NAPI_RETURN_AND_UNREF_STRING(name) \ - napi_value return_value; \ - if (name == NULL) { \ - NAPI_STATUS_THROWS(napi_get_null(env, &return_value)); \ - return return_value; \ - } \ - NAPI_STATUS_THROWS(napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &return_value)); \ - dc_str_unref(name); \ - return return_value; - -#define NAPI_ASYNC_CARRIER_BEGIN(name) \ - typedef struct name##_carrier_t { \ - napi_ref callback_ref; \ - napi_async_work async_work; \ - dcn_context_t* dcn_context; - -#define NAPI_ASYNC_CARRIER_END(name) \ - } name##_carrier_t; - -#define NAPI_ASYNC_EXECUTE(name) \ - static void name##_execute(napi_env env, void* data) - -#define NAPI_ASYNC_GET_CARRIER(name) \ - name##_carrier_t* carrier = (name##_carrier_t*)data; - -#define NAPI_ASYNC_COMPLETE(name) \ - static void name##_complete(napi_env env, napi_status status, void* data) - -#define NAPI_ASYNC_CALL_AND_DELETE_CB() \ - napi_value global; \ - NAPI_STATUS_THROWS(napi_get_global(env, &global)); \ - napi_value callback; \ - NAPI_STATUS_THROWS(napi_get_reference_value(env, carrier->callback_ref, &callback)); \ - NAPI_STATUS_THROWS(napi_call_function(env, global, callback, argc, argv, NULL)); \ - NAPI_STATUS_THROWS(napi_delete_reference(env, carrier->callback_ref)); \ - NAPI_STATUS_THROWS(napi_delete_async_work(env, carrier->async_work)); - -#define NAPI_ASYNC_NEW_CARRIER(name) \ - name##_carrier_t* carrier = calloc(1, sizeof(name##_carrier_t)); \ - carrier->dcn_context = dcn_context; - -#define NAPI_ASYNC_QUEUE_WORK(name, cb) \ - napi_value callback = cb; \ - napi_value async_resource_name; \ - NAPI_STATUS_THROWS(napi_create_reference(env, callback, 1, &carrier->callback_ref)); \ - NAPI_STATUS_THROWS(napi_create_string_utf8(env, #name "_callback", \ - NAPI_AUTO_LENGTH, \ - &async_resource_name)); \ - NAPI_STATUS_THROWS(napi_create_async_work(env, callback, async_resource_name, \ - name##_execute, name##_complete, \ - carrier, &carrier->async_work)); \ - NAPI_STATUS_THROWS(napi_queue_async_work(env, carrier->async_work)); - -/*** this could/should be moved to napi-macros ***/ - -#define NAPI_DOUBLE(name, val) \ - double name; \ - if (napi_get_value_double(env, val, &name) != napi_ok) { \ - napi_throw_error(env, "EINVAL", "Expected double"); \ - return NULL; \ - } - -#define NAPI_ARGV_DOUBLE(name, i) \ - NAPI_DOUBLE(name, argv[i]) diff --git a/node/test/fixtures/avatar.png b/node/test/fixtures/avatar.png deleted file mode 100644 index 76e69ce492..0000000000 Binary files a/node/test/fixtures/avatar.png and /dev/null differ diff --git a/node/test/fixtures/image.jpeg b/node/test/fixtures/image.jpeg deleted file mode 100644 index 7e64c99307..0000000000 Binary files a/node/test/fixtures/image.jpeg and /dev/null differ diff --git a/node/test/fixtures/logo.png b/node/test/fixtures/logo.png deleted file mode 100644 index 9408ad90b4..0000000000 Binary files a/node/test/fixtures/logo.png and /dev/null differ diff --git a/node/test/test.mjs b/node/test/test.mjs deleted file mode 100644 index 11e4703adb..0000000000 --- a/node/test/test.mjs +++ /dev/null @@ -1,943 +0,0 @@ -// @ts-check -import { DeltaChat } from '../dist/index.js' - -import { deepStrictEqual, strictEqual } from 'assert' -import chai, { expect } from 'chai' -import chaiAsPromised from 'chai-as-promised' -import { EventId2EventName, C } from '../dist/constants.js' -import { join } from 'path' -import { statSync } from 'fs' -import { Context } from '../dist/context.js' -import { fileURLToPath } from 'url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); - -chai.use(chaiAsPromised) -chai.config.truncateThreshold = 0 // Do not truncate assertion errors. - -function createTempUser(chatmailDomain) { - const charset = "2345789acdefghjkmnpqrstuvwxyz"; - let user = "ci-"; - for (let i = 0; i < 6; i++) { - user += charset[Math.floor(Math.random() * charset.length)]; - } - const email = user + "@" + chatmailDomain; - return { email: email, password: user + "$" + user }; -} - -describe('static tests', function () { - this.timeout(60 * 5 * 1000) // increase timeout to 5 min - - it('reverse lookup of events', function () { - const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k)) - const eventValues = Object.values(EventId2EventName) - const reverse = eventValues.map((v) => C[v]) - expect(reverse).to.be.deep.equal(eventKeys) - }) - - it('event constants are consistent', function () { - const eventKeys = Object.keys(C) - .filter((k) => k.startsWith('DC_EVENT_')) - .sort() - const eventValues = Object.values(EventId2EventName).sort() - expect(eventKeys).to.be.deep.equal(eventValues) - }) - - it('static method maybeValidAddr()', function () { - expect(DeltaChat.maybeValidAddr(null)).to.equal(false) - expect(DeltaChat.maybeValidAddr('')).to.equal(false) - expect(DeltaChat.maybeValidAddr('uuu')).to.equal(false) - expect(DeltaChat.maybeValidAddr('dd.tt')).to.equal(false) - expect(DeltaChat.maybeValidAddr('tt.dd@yggmail')).to.equal(true) - expect(DeltaChat.maybeValidAddr('u@d')).to.equal(true) - //expect(DeltaChat.maybeValidAddr('u@d.')).to.equal(false) - //expect(DeltaChat.maybeValidAddr('u@d.t')).to.equal(false) - //expect(DeltaChat.maybeValidAddr('u@.tt')).to.equal(false) - expect(DeltaChat.maybeValidAddr('@d.tt')).to.equal(false) - expect(DeltaChat.maybeValidAddr('user@domain.tld')).to.equal(true) - expect(DeltaChat.maybeValidAddr('u@d.tt')).to.equal(true) - }) - - it('static getSystemInfo()', function () { - const info = Context.getSystemInfo() - expect(info).to.contain.keys([ - 'arch', - 'deltachat_core_version', - 'sqlite_version', - ]) - }) - - it('static context.getProviderFromEmail("example@example.com")', function () { - const provider = DeltaChat.getProviderFromEmail('example@example.com') - - expect(provider).to.deep.equal({ - before_login_hint: "Hush this provider doesn't exist!", - overview_page: 'https://providers.delta.chat/example-com', - status: 3, - }) - }) -}) - -describe('JSON RPC', function () { - it('smoketest', async function () { - const { dc } = DeltaChat.newTemporary() - let promise_resolve - const promise = new Promise((res, _rej) => { - promise_resolve = (response) => { - // ignore events - const answer = JSON.parse(response) - if (answer['method'] !== 'event') res(answer) - } - }) - dc.startJsonRpcHandler(promise_resolve) - dc.jsonRpcRequest( - JSON.stringify({ - jsonrpc: '2.0', - method: 'get_all_account_ids', - params: [], - id: 2, - }) - ) - deepStrictEqual( - { - jsonrpc: '2.0', - id: 2, - result: [1], - }, - await promise - ) - dc.close() - }) - - it('basic test', async function () { - const { dc } = DeltaChat.newTemporary() - - const promises = {} - dc.startJsonRpcHandler((msg) => { - const response = JSON.parse(msg) - if (response.hasOwnProperty('id')) promises[response.id](response) - delete promises[response.id] - }) - const call = (request) => { - dc.jsonRpcRequest(JSON.stringify(request)) - return new Promise((res, _rej) => { - promises[request.id] = res - }) - } - - deepStrictEqual( - { - jsonrpc: '2.0', - id: 2, - result: [1], - }, - await call({ - jsonrpc: '2.0', - method: 'get_all_account_ids', - params: [], - id: 2, - }) - ) - - deepStrictEqual( - { - jsonrpc: '2.0', - id: 3, - result: 2, - }, - await call({ - jsonrpc: '2.0', - method: 'add_account', - params: [], - id: 3, - }) - ) - - deepStrictEqual( - { - jsonrpc: '2.0', - id: 4, - result: [1, 2], - }, - await call({ - jsonrpc: '2.0', - method: 'get_all_account_ids', - params: [], - id: 4, - }) - ) - - dc.close() - }) -}) - -describe('Basic offline Tests', function () { - it('opens a context', async function () { - const { dc, context } = DeltaChat.newTemporary() - - strictEqual(context.isConfigured(), false) - dc.close() - }) - - it('set config', async function () { - const { dc, context } = DeltaChat.newTemporary() - - context.setConfig('bot', true) - strictEqual(context.getConfig('bot'), '1') - context.setConfig('bot', false) - strictEqual(context.getConfig('bot'), '0') - context.setConfig('bot', '1') - strictEqual(context.getConfig('bot'), '1') - context.setConfig('bot', '0') - strictEqual(context.getConfig('bot'), '0') - context.setConfig('bot', 1) - strictEqual(context.getConfig('bot'), '1') - context.setConfig('bot', 0) - strictEqual(context.getConfig('bot'), '0') - - context.setConfig('bot', null) - strictEqual(context.getConfig('bot'), '') - - strictEqual(context.getConfig('selfstatus'), '') - context.setConfig('selfstatus', 'hello') - strictEqual(context.getConfig('selfstatus'), 'hello') - context.setConfig('selfstatus', '') - strictEqual(context.getConfig('selfstatus'), '') - context.setConfig('selfstatus', null) - strictEqual(context.getConfig('selfstatus'), '') - - dc.close() - }) - - it('configure with either missing addr or missing mail_pw throws', async function () { - const { dc, context } = DeltaChat.newTemporary() - dc.startEvents() - - await expect( - context.configure({ addr: 'delta1@delta.localhost' }) - ).to.eventually.be.rejectedWith('Missing (IMAP) password.') - await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be - .rejected - - context.stopOngoingProcess() - dc.close() - }) - - it('context.getInfo()', async function () { - const { dc, context } = DeltaChat.newTemporary() - - const info = await context.getInfo() - expect(typeof info).to.be.equal('object') - expect(info).to.contain.keys([ - 'arch', - 'bcc_self', - 'blobdir', - 'bot', - 'configured_mvbox_folder', - 'configured_sentbox_folder', - 'database_dir', - 'database_encrypted', - 'database_version', - 'delete_device_after', - 'delete_server_after', - 'deltachat_core_version', - 'displayname', - 'download_limit', - 'e2ee_enabled', - 'entered_account_settings', - 'fetch_existing_msgs', - 'fingerprint', - 'folders_configured', - 'is_configured', - 'journal_mode', - 'key_gen_type', - 'last_housekeeping', - 'last_cant_decrypt_outgoing_msgs', - 'level', - 'mdns_enabled', - 'media_quality', - 'messages_in_contact_requests', - 'mvbox_move', - 'num_cpus', - 'number_of_chat_messages', - 'number_of_chats', - 'number_of_contacts', - 'only_fetch_mvbox', - 'private_key_count', - 'public_key_count', - 'quota_exceeding', - 'scan_all_folders_debounce_secs', - 'selfavatar', - 'sync_msgs', - 'sentbox_watch', - 'show_emails', - 'proxy_enabled', - 'sqlite_version', - 'uptime', - 'used_account_settings', - 'webrtc_instance', - ]) - - dc.close() - }) -}) - -describe('Offline Tests with unconfigured account', function () { - let [dc, context, accountId, directory] = [null, null, null, null] - - this.beforeEach(async function () { - let tmp = DeltaChat.newTemporary() - dc = tmp.dc - context = tmp.context - accountId = tmp.accountId - directory = tmp.directory - dc.startEvents() - }) - - this.afterEach(async function () { - if (context) { - context.stopOngoingProcess() - } - if (dc) { - try { - dc.stopIO() - dc.close() - } catch (error) { - console.error(error) - } - } - - dc = null - context = null - accountId = null - directory = null - }) - - it('invalid context.joinSecurejoin', async function () { - expect(context.joinSecurejoin('test')).to.be.eq(0) - }) - - it('Device Chat', async function () { - const deviceChatMessageText = 'test234' - - expect((await context.getChatList(0, '', null)).getCount()).to.equal( - 0, - 'no device chat after setup' - ) - - await context.addDeviceMessage('test', deviceChatMessageText) - - const chatList = await context.getChatList(0, '', null) - expect(chatList.getCount()).to.equal( - 1, - 'device chat after adding device msg' - ) - - const deviceChatId = await chatList.getChatId(0) - const deviceChat = await context.getChat(deviceChatId) - expect(deviceChat.isDeviceTalk()).to.be.true - expect(deviceChat.toJson().isDeviceTalk).to.be.true - - const deviceChatMessages = await context.getChatMessages(deviceChatId, 0, 0) - expect(deviceChatMessages.length).to.be.equal( - 1, - 'device chat has added message' - ) - - const deviceChatMessage = await context.getMessage(deviceChatMessages[0]) - expect(deviceChatMessage.getText()).to.equal( - deviceChatMessageText, - 'device chat message has the inserted text' - ) - }) - - it('should have e2ee enabled and right blobdir', function () { - expect(context.getConfig('e2ee_enabled')).to.equal( - '1', - 'e2eeEnabled correct' - ) - expect( - String(context.getBlobdir()).startsWith(directory), - 'blobdir should be inside temp directory' - ) - expect( - String(context.getBlobdir()).endsWith('db.sqlite-blobs'), - 'blobdir end with "db.sqlite-blobs"' - ) - }) - - it('should create chat from contact and Chat methods', async function () { - const contactId = context.createContact('aaa', 'aaa@site.org') - - strictEqual(context.lookupContactIdByAddr('aaa@site.org'), contactId) - strictEqual(context.lookupContactIdByAddr('nope@site.net'), 0) - - let chatId = context.createChatByContactId(contactId) - let chat = context.getChat(chatId) - - strictEqual( - chat.getVisibility(), - C.DC_CHAT_VISIBILITY_NORMAL, - 'not archived' - ) - strictEqual(chat.getId(), chatId, 'chat id matches') - strictEqual(chat.getName(), 'aaa', 'chat name matches') - strictEqual(chat.getProfileImage(), null, 'no profile image') - strictEqual(chat.getType(), C.DC_CHAT_TYPE_SINGLE, 'single chat') - strictEqual(chat.isSelfTalk(), false, 'no self talk') - // TODO make sure this is really the case! - strictEqual(chat.isUnpromoted(), false, 'not unpromoted') - strictEqual(chat.isProtected(), false, 'not verified') - strictEqual(typeof chat.color, 'string', 'color is a string') - - strictEqual(context.getDraft(chatId), null, 'no draft message') - context.setDraft(chatId, context.messageNew().setText('w00t!')) - strictEqual( - context.getDraft(chatId).toJson().text, - 'w00t!', - 'draft text correct' - ) - context.setDraft(chatId, null) - strictEqual(context.getDraft(chatId), null, 'draft removed') - - strictEqual(context.getChatIdByContactId(contactId), chatId) - expect(context.getChatContacts(chatId)).to.deep.equal([contactId]) - - context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED) - strictEqual( - context.getChat(chatId).getVisibility(), - C.DC_CHAT_VISIBILITY_ARCHIVED, - 'chat archived' - ) - context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL) - strictEqual( - chat.getVisibility(), - C.DC_CHAT_VISIBILITY_NORMAL, - 'chat unarchived' - ) - - chatId = context.createGroupChat('unverified group', false) - chat = context.getChat(chatId) - strictEqual(chat.isProtected(), false, 'is not verified') - strictEqual(chat.getType(), C.DC_CHAT_TYPE_GROUP, 'group chat') - expect(context.getChatContacts(chatId)).to.deep.equal([ - C.DC_CONTACT_ID_SELF, - ]) - - const draft2 = context.getDraft(chatId) - expect(draft2 == null, 'unpromoted group has no draft by default') - - context.setChatName(chatId, 'NEW NAME') - strictEqual(context.getChat(chatId).getName(), 'NEW NAME', 'name updated') - - chatId = context.createGroupChat('a verified group', true) - chat = context.getChat(chatId) - strictEqual(chat.isProtected(), true, 'is verified') - }) - - it('test setting profile image', async function () { - const chatId = context.createGroupChat('testing profile image group', false) - const image = 'image.jpeg' - const imagePath = join(__dirname, 'fixtures', image) - const blobs = context.getBlobdir() - - context.setChatProfileImage(chatId, imagePath) - const blobPath = context.getChat(chatId).getProfileImage() - expect(blobPath.startsWith(blobs)).to.be.true - expect(blobPath.includes('image')).to.be.false - expect(blobPath.endsWith('.jpeg')).to.be.true - - context.setChatProfileImage(chatId, null) - expect(context.getChat(chatId).getProfileImage()).to.be.equal( - null, - 'image is null' - ) - }) - - it('test setting ephemeral timer', function () { - const chatId = context.createGroupChat('testing ephemeral timer') - - strictEqual( - context.getChatEphemeralTimer(chatId), - 0, - 'ephemeral timer is not set by default' - ) - - context.setChatEphemeralTimer(chatId, 60) - strictEqual( - context.getChatEphemeralTimer(chatId), - 60, - 'ephemeral timer is set to 1 minute' - ) - - context.setChatEphemeralTimer(chatId, 0) - strictEqual( - context.getChatEphemeralTimer(chatId), - 0, - 'ephemeral timer is reset' - ) - }) - - it('should create and delete chat', function () { - const chatId = context.createGroupChat('GROUPCHAT') - const chat = context.getChat(chatId) - strictEqual(chat.getId(), chatId, 'correct chatId') - context.deleteChat(chat.getId()) - strictEqual(context.getChat(chatId), null, 'chat removed') - }) - - it('new message and Message methods', function () { - const text = 'w00t!' - const msg = context.messageNew().setText(text) - - strictEqual(msg.getChatId(), 0, 'chat id 0 before sent') - strictEqual(msg.getDuration(), 0, 'duration 0 before sent') - strictEqual(msg.getFile(), '', 'no file set by default') - strictEqual(msg.getFilebytes(), 0, 'and file bytes is 0') - strictEqual(msg.getFilemime(), '', 'no filemime by default') - strictEqual(msg.getFilename(), '', 'no filename set by default') - strictEqual(msg.getFromId(), 0, 'no contact id set by default') - strictEqual(msg.getHeight(), 0, 'plain text message have height 0') - strictEqual(msg.getId(), 0, 'id 0 before sent') - strictEqual(msg.getSetupcodebegin(), '', 'no setupcode begin') - strictEqual(msg.getShowpadlock(), false, 'no padlock by default') - - const state = msg.getState() - strictEqual(state.isUndefined(), true, 'no state by default') - strictEqual(state.isFresh(), false, 'no state by default') - strictEqual(state.isNoticed(), false, 'no state by default') - strictEqual(state.isSeen(), false, 'no state by default') - strictEqual(state.isPending(), false, 'no state by default') - strictEqual(state.isFailed(), false, 'no state by default') - strictEqual(state.isDelivered(), false, 'no state by default') - strictEqual(state.isReceived(), false, 'no state by default') - - const summary = msg.getSummary() - strictEqual(summary.getId(), 0, 'no summary id') - strictEqual(summary.getState(), 0, 'no summary state') - strictEqual(summary.getText1(), null, 'no summary text1') - strictEqual(summary.getText1Meaning(), 0, 'no summary text1 meaning') - strictEqual(summary.getText2(), '', 'no summary text2') - strictEqual(summary.getTimestamp(), 0, 'no summary timestamp') - - //strictEqual(msg.getSummarytext(50), text, 'summary text is text') - strictEqual(msg.getText(), text, 'msg text set correctly') - strictEqual(msg.getTimestamp(), 0, 'no timestamp') - - const viewType = msg.getViewType() - strictEqual(viewType.isText(), true) - strictEqual(viewType.isImage(), false) - strictEqual(viewType.isGif(), false) - strictEqual(viewType.isAudio(), false) - strictEqual(viewType.isVoice(), false) - strictEqual(viewType.isVideo(), false) - strictEqual(viewType.isFile(), false) - - strictEqual(msg.getWidth(), 0, 'no message width') - strictEqual(msg.isDeadDrop(), false, 'not deaddrop') - strictEqual(msg.isForwarded(), false, 'not forwarded') - strictEqual(msg.isInfo(), false, 'not an info message') - strictEqual(msg.isSent(), false, 'messge is not sent') - strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message') - - msg.latefilingMediasize(10, 20, 30) - strictEqual(msg.getWidth(), 10, 'message width set correctly') - strictEqual(msg.getHeight(), 20, 'message height set correctly') - strictEqual(msg.getDuration(), 30, 'message duration set correctly') - - msg.setDimension(100, 200) - strictEqual(msg.getWidth(), 100, 'message width set correctly') - strictEqual(msg.getHeight(), 200, 'message height set correctly') - - msg.setDuration(314) - strictEqual(msg.getDuration(), 314, 'message duration set correctly') - - expect(() => { - msg.setFile(null) - }).to.throw('Missing filename') - - const logo = join(__dirname, 'fixtures', 'logo.png') - const stat = statSync(logo) - msg.setFile(logo) - strictEqual(msg.getFilebytes(), stat.size, 'correct file size') - strictEqual(msg.getFile(), logo, 'correct file name') - strictEqual(msg.getFilemime(), 'image/png', 'mime set implicitly') - msg.setFile(logo, 'image/gif') - strictEqual(msg.getFilemime(), 'image/gif', 'mime set (in)correctly') - msg.setFile(logo, 'image/png') - strictEqual(msg.getFilemime(), 'image/png', 'mime set correctly') - - const json = msg.toJson() - expect(json).to.not.equal(null, 'not null') - strictEqual(typeof json, 'object', 'json object') - }) - - it('Contact methods', function () { - const contactId = context.createContact('First Last', 'first.last@site.org') - const contact = context.getContact(contactId) - - strictEqual(contact.getAddress(), 'first.last@site.org', 'correct address') - strictEqual(typeof contact.color, 'string', 'color is a string') - strictEqual(contact.getDisplayName(), 'First Last', 'correct display name') - strictEqual(contact.getId(), contactId, 'contact id matches') - strictEqual(contact.getName(), 'First Last', 'correct name') - strictEqual(contact.getNameAndAddress(), 'First Last (first.last@site.org)') - strictEqual(contact.getProfileImage(), null, 'no contact image') - strictEqual(contact.isBlocked(), false, 'not blocked') - strictEqual(contact.isVerified(), false, 'unverified status') - strictEqual(contact.lastSeen, 0, 'last seen unknown') - }) - - it('create contacts from address book', function () { - const addresses = [ - 'Name One', - 'name1@site.org', - 'Name Two', - 'name2@site.org', - 'Name Three', - 'name3@site.org', - ] - const count = context.addAddressBook(addresses.join('\n')) - strictEqual(count, addresses.length / 2) - context - .getContacts(0, 'Name ') - .map((id) => context.getContact(id)) - .forEach((contact) => { - expect(contact.getName().startsWith('Name ')).to.be.true - }) - }) - - it('delete contacts', function () { - const id = context.createContact('someuser', 'someuser@site.com') - const contact = context.getContact(id) - strictEqual(contact.getId(), id, 'contact id matches') - context.deleteContact(id) - strictEqual(context.getContact(id), null, 'contact is gone') - }) - - it('adding and removing a contact from a chat', function () { - const chatId = context.createGroupChat('adding_and_removing') - const contactId = context.createContact('Add Remove', 'add.remove@site.com') - strictEqual( - context.addContactToChat(chatId, contactId), - true, - 'contact added' - ) - strictEqual( - context.isContactInChat(chatId, contactId), - true, - 'contact in chat' - ) - strictEqual( - context.removeContactFromChat(chatId, contactId), - true, - 'contact removed' - ) - strictEqual( - context.isContactInChat(chatId, contactId), - false, - 'contact not in chat' - ) - }) - - it('blocking contacts', function () { - const id = context.createContact('badcontact', 'bad@site.com') - - strictEqual(context.getBlockedCount(), 0) - strictEqual(context.getContact(id).isBlocked(), false) - expect(context.getBlockedContacts()).to.be.empty - - context.blockContact(id, true) - strictEqual(context.getBlockedCount(), 1) - strictEqual(context.getContact(id).isBlocked(), true) - expect(context.getBlockedContacts()).to.deep.equal([id]) - - context.blockContact(id, false) - strictEqual(context.getBlockedCount(), 0) - strictEqual(context.getContact(id).isBlocked(), false) - expect(context.getBlockedContacts()).to.be.empty - }) - - it('ChatList methods', function () { - const ids = [ - context.createGroupChat('groupchat1'), - context.createGroupChat('groupchat11'), - context.createGroupChat('groupchat111'), - ] - - let chatList = context.getChatList(0, 'groupchat1', null) - strictEqual(chatList.getCount(), 3, 'should contain above chats') - expect(ids.indexOf(chatList.getChatId(0))).not.to.equal(-1) - expect(ids.indexOf(chatList.getChatId(1))).not.to.equal(-1) - expect(ids.indexOf(chatList.getChatId(2))).not.to.equal(-1) - - const lot = chatList.getSummary(0) - strictEqual(lot.getId(), 0, 'lot has no id') - strictEqual(lot.getState(), C.DC_STATE_IN_NOTICED, 'correct state') - - const text = 'Others will only see this group after you sent a first message.' - context.createGroupChat('groupchat1111') - chatList = context.getChatList(0, 'groupchat1111', null) - strictEqual( - chatList.getSummary(0).getText2(), - text, - 'custom new group message' - ) - - context.setChatVisibility(ids[0], C.DC_CHAT_VISIBILITY_ARCHIVED) - chatList = context.getChatList(C.DC_GCL_ARCHIVED_ONLY, 'groupchat1', null) - strictEqual(chatList.getCount(), 1, 'only one archived') - }) - - it('Remove quote from (draft) message', function () { - context.addDeviceMessage('test_quote', 'test') - const msgId = context.getChatMessages(10, 0, 0)[0] - const msg = context.messageNew() - - msg.setQuote(context.getMessage(msgId)) - expect(msg.getQuotedMessage()).to.not.be.null - msg.setQuote(null) - expect(msg.getQuotedMessage()).to.be.null - }) -}) - -describe('Integration tests', function () { - this.timeout(60 * 5 * 1000) // increase timeout to 5 min - - let [dc, context, accountId, directory, account] = [ - null, - null, - null, - null, - null, - ] - - let [dc2, context2, accountId2, directory2, account2] = [ - null, - null, - null, - null, - null, - ] - - this.beforeEach(async function () { - let tmp = DeltaChat.newTemporary() - dc = tmp.dc - context = tmp.context - accountId = tmp.accountId - directory = tmp.directory - dc.startEvents() - }) - - this.afterEach(async function () { - if (context) { - try { - context.stopOngoingProcess() - } catch (error) { - console.error(error) - } - } - if (context2) { - try { - context2.stopOngoingProcess() - } catch (error) { - console.error(error) - } - } - - if (dc) { - try { - dc.stopIO() - dc.close() - } catch (error) { - console.error(error) - } - } - - dc = null - context = null - accountId = null - directory = null - - context2 = null - accountId2 = null - directory2 = null - }) - - this.beforeAll(async function () { - account = createTempUser(process.env.CHATMAIL_DOMAIN) - if (!account || !account.email || !account.password) { - console.log( - "We didn't got back an account from the api, skip integration tests" - ) - this.skip() - } - }) - - it('configure', async function () { - strictEqual(context.isConfigured(), false, 'should not be configured') - - // Not sure what's the best way to check the events - // TODO: check the events - - // dc.once('DC_EVENT_CONFIGURE_PROGRESS', (data) => { - // t.pass('DC_EVENT_CONFIGURE_PROGRESS called at least once') - // }) - // dc.on('DC_EVENT_ERROR', (error) => { - // console.error('DC_EVENT_ERROR', error) - // }) - // dc.on('DC_EVENT_ERROR_NETWORK', (first, error) => { - // console.error('DC_EVENT_ERROR_NETWORK', error) - // }) - - // dc.on('ALL', (event, data1, data2) => console.log('ALL', event, data1, data2)) - - await expect( - context.configure({ - addr: account.email, - mail_pw: account.password, - - displayname: 'Delta One', - selfstatus: 'From Delta One with <3', - selfavatar: join(__dirname, 'fixtures', 'avatar.png'), - }) - ).to.be.eventually.fulfilled - - strictEqual(context.getConfig('addr'), account.email, 'addr correct') - strictEqual( - context.getConfig('displayname'), - 'Delta One', - 'displayName correct' - ) - strictEqual( - context.getConfig('selfstatus'), - 'From Delta One with <3', - 'selfStatus correct' - ) - expect( - context.getConfig('selfavatar').endsWith('avatar.png'), - 'selfavatar correct' - ) - strictEqual(context.getConfig('e2ee_enabled'), '1', 'e2ee_enabled correct') - strictEqual( - context.getConfig('save_mime_headers'), - '', - 'save_mime_headers correct' - ) - - expect(context.getBlobdir().endsWith('db.sqlite-blobs'), 'correct blobdir') - strictEqual(context.isConfigured(), true, 'is configured') - - // whole re-configure to only change displayname: what the heck? (copied this from the old test) - await expect( - context.configure({ - addr: account.email, - mail_pw: account.password, - displayname: 'Delta Two', - selfstatus: 'From Delta One with <3', - selfavatar: join(__dirname, 'fixtures', 'avatar.png'), - }) - ).to.be.eventually.fulfilled - strictEqual( - context.getConfig('displayname'), - 'Delta Two', - 'updated displayName correct' - ) - }) - - it('Autocrypt setup - key transfer', async function () { - // Spawn a second dc instance with same account - // dc.on('ALL', (event, data1, data2) => - // console.log('FIRST ', event, data1, data2) - // ) - dc.stopIO() - await expect( - context.configure({ - addr: account.email, - mail_pw: account.password, - - displayname: 'Delta One', - selfstatus: 'From Delta One with <3', - selfavatar: join(__dirname, 'fixtures', 'avatar.png'), - }) - ).to.be.eventually.fulfilled - - const accountId2 = dc.addAccount() - console.log('accountId2:', accountId2) - context2 = dc.accountContext(accountId2) - - let setupCode = null - const waitForSetupCode = waitForSomething() - const waitForEnd = waitForSomething() - - dc.on('ALL', (event, accountId, data1, data2) => { - console.log('[' + accountId + ']', event, data1, data2) - }) - - dc.on('DC_EVENT_MSGS_CHANGED', async (aId, chatId, msgId) => { - console.log('[' + accountId + '] DC_EVENT_MSGS_CHANGED', chatId, msgId) - if ( - aId != accountId || - !context.getChat(chatId).isSelfTalk() || - !context.getMessage(msgId).isSetupmessage() - ) { - return - } - console.log('Setupcode!') - let setupCode = await waitForSetupCode.promise - // console.log('incoming msg', { setupCode }) - const messages = context.getChatMessages(chatId, 0, 0) - expect(messages.indexOf(msgId) !== -1, 'msgId is in chat messages').to.be - .true - const result = await context.continueKeyTransfer(msgId, setupCode) - expect(result === true, 'continueKeyTransfer was successful').to.be.true - - waitForEnd.done() - }) - - dc.stopIO() - await expect( - context2.configure({ - addr: account.email, - mail_pw: account.password, - - displayname: 'Delta One', - selfstatus: 'From Delta One with <3', - selfavatar: join(__dirname, 'fixtures', 'avatar.png'), - }) - ).to.be.eventually.fulfilled - dc.startIO() - - console.log('Sending autocrypt setup code') - setupCode = await context2.initiateKeyTransfer() - console.log('Sent autocrypt setup code') - waitForSetupCode.done(setupCode) - console.log('setupCode is: ' + setupCode) - expect(typeof setupCode).to.equal('string', 'setupCode is string') - - await waitForEnd.promise - }) - - it('configure using invalid password should fail', async function () { - await expect( - context.configure({ - addr: 'hpk5@testrun.org', - mail_pw: 'asd', - }) - ).to.be.eventually.rejected - }) -}) - -/** - * @returns {{done: (result?)=>void, promise:Promise }} - */ -function waitForSomething() { - let resolvePromise - const promise = new Promise((res, rej) => { - resolvePromise = res - }) - return { - done: resolvePromise, - promise, - } -} diff --git a/node/tsconfig.json b/node/tsconfig.json deleted file mode 100644 index 25ce300f26..0000000000 --- a/node/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "rootDir": "./lib", - "sourceMap": true, - "module": "commonjs", - "target": "es5", - "esModuleInterop": true, - "declaration": true, - "declarationMap": true, - "strictNullChecks": true, - "strict": true - }, - "exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"], - "typedocOptions": { - "out": "docs", - "excludePrivate": true, - "defaultCategory": "index", - "includeVersion": true, - "entryPoints": ["lib/index.ts"] - } -} \ No newline at end of file diff --git a/node/windows.md b/node/windows.md deleted file mode 100644 index df19b03d7b..0000000000 --- a/node/windows.md +++ /dev/null @@ -1,37 +0,0 @@ -> Steps on how to get windows set up properly for the node bindings - -## install git - -E.g via - -## install node - -Download and install `v18` from - -## install rust - -Download and run `rust-init.exe` from - -## configure node for native addons - -``` -$ npm i node-gyp -g -$ npm i windows-build-tools -g -``` - -`windows-build-tools` will install `Visual Studio 2017` by default and should not mess with existing installations of `Visual Studio C++`. - -## get the code - -``` -$ mkdir -p src/deltachat -$ cd src/deltachat -$ git clone https://github.com/deltachat/deltachat-node -``` - -## build the code - -``` -$ cd src/deltachat/deltachat-node -$ npm install -``` diff --git a/package.json b/package.json deleted file mode 100644 index f345b9a9e8..0000000000 --- a/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "dependencies": { - "debug": "^4.1.1", - "napi-macros": "^2.0.0", - "node-gyp-build": "^4.6.1" - }, - "description": "node.js bindings for deltachat-core", - "devDependencies": { - "@types/debug": "^4.1.7", - "@types/node": "^20.8.10", - "chai": "~4.3.10", - "chai-as-promised": "^7.1.1", - "mocha": "^8.2.1", - "node-gyp": "~10.1.0", - "prebuildify": "^5.0.1", - "prebuildify-ci": "^1.0.5", - "prettier": "^3.0.3", - "typedoc": "^0.25.3", - "typescript": "^5.2.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "node/scripts/*", - "*" - ], - "homepage": "https://github.com/deltachat/deltachat-core-rust/tree/master/node", - "license": "GPL-3.0-or-later", - "main": "node/dist/index.js", - "name": "deltachat-node", - "repository": { - "type": "git", - "url": "https://github.com/deltachat/deltachat-core-rust.git" - }, - "scripts": { - "build": "npm run build:core && npm run build:bindings", - "build:bindings": "npm run build:bindings:c && npm run build:bindings:ts", - "build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall", - "build:bindings:c:c": "cd node && node-gyp rebuild", - "build:bindings:c:postinstall": "node node/scripts/postinstall.js", - "build:bindings:ts": "cd node && tsc", - "build:core": "npm run build:core:rust && npm run build:core:constants", - "build:core:constants": "node node/scripts/generate-constants.js", - "build:core:rust": "node node/scripts/rebuild-core.js", - "clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target", - "download-prebuilds": "prebuildify-ci download", - "install": "node node/scripts/install.js", - "install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"", - "lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"", - "lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"", - "prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"", - "test": "npm run test:lint && npm run test:mocha", - "test:lint": "npm run lint", - "test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit" - }, - "types": "node/dist/index.d.ts", - "version": "1.155.2" -} diff --git a/python/README.rst b/python/README.rst index 00440fb35c..dd1c6317f1 100644 --- a/python/README.rst +++ b/python/README.rst @@ -2,9 +2,9 @@ CFFI Python Bindings ============================ -This package provides `Python bindings`_ to the `deltachat-core library`_ +This package provides `Python bindings`_ to the `chatmail core library`_ which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers a low-level Chat/Contact/Message API to user interfaces and bots. -.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust +.. _`chatmail core library`: https://github.com/chatmail/core .. _`Python bindings`: https://py.delta.chat/ diff --git a/python/doc/cffi/install.rst b/python/doc/cffi/install.rst index 5a77557a60..db153020e2 100644 --- a/python/doc/cffi/install.rst +++ b/python/doc/cffi/install.rst @@ -43,7 +43,7 @@ Bootstrap Rust and Cargo by using rustup:: Then clone the deltachat-core-rust repo:: - git clone https://github.com/deltachat/deltachat-core-rust + git clone https://github.com/chatmail/core cd deltachat-core-rust To install the Delta Chat Python bindings make sure you have Python3 installed. diff --git a/python/doc/index.rst b/python/doc/index.rst index 1a95370d3e..e3fa519d41 100644 --- a/python/doc/index.rst +++ b/python/doc/index.rst @@ -2,7 +2,7 @@ Delta Chat Python bindings, new and old ======= `Delta Chat `_ provides two kinds of Python bindings -to the `Rust Core `_: +to the `Rust Core `_: JSON-RPC bindings and CFFI bindings. When starting a new project it is recommended to use JSON-RPC bindings, which are used in the Delta Chat Desktop app through generated Typescript-bindings. @@ -41,4 +41,4 @@ as the CFFI bindings are increasingly in maintenance-only mode. .. _virtualenv: http://pypi.org/project/virtualenv/ .. _merlinux: http://merlinux.eu .. _pypi: http://pypi.org/ -.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust +.. _`issue-tracker`: https://github.com/chatmail/core diff --git a/python/doc/jsonrpc/develop.rst b/python/doc/jsonrpc/develop.rst index 0ac25e606a..b9c044acc7 100644 --- a/python/doc/jsonrpc/develop.rst +++ b/python/doc/jsonrpc/develop.rst @@ -3,9 +3,9 @@ Development =========== To develop JSON-RPC bindings, -clone the `deltachat-core-rust `_ repository:: +clone the `chatmail core `_ repository:: - git clone https://github.com/deltachat/deltachat-core-rust.git + git clone https://github.com/chatmail/core.git Testing ======= diff --git a/python/doc/jsonrpc/install.rst b/python/doc/jsonrpc/install.rst index 9310009198..82daf44f59 100644 --- a/python/doc/jsonrpc/install.rst +++ b/python/doc/jsonrpc/install.rst @@ -17,8 +17,8 @@ Install ``deltachat-rpc-server`` To get ``deltachat-rpc-server`` binary you have three options: 1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``. -2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``. -3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``. +2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``. +3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``. Check that ``deltachat-rpc-server`` is installed and can run:: @@ -33,4 +33,4 @@ Install ``deltachat-rpc-client`` To get ``deltachat-rpc-client`` Python library you can: 1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``. -2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``. +2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/chatmail/core.git@main#subdirectory=deltachat-rpc-client``. diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py index 943fae0bb7..7e55d6617c 100644 --- a/python/examples/test_examples.py +++ b/python/examples/test_examples.py @@ -25,8 +25,8 @@ def test_echo_quit_plugin(acfactory, lp): (ac1,) = acfactory.get_online_accounts(1) lp.sec("sending a message to the bot") - bot_contact = ac1.create_contact(botproc.addr) - bot_chat = bot_contact.create_chat() + bot_chat = ac1.qr_setup_contact(botproc.qr) + ac1._evtracker.wait_securejoin_joiner_progress(1000) bot_chat.send_text("hello") lp.sec("waiting for the reply message from the bot to arrive") @@ -48,7 +48,9 @@ def test_group_tracking_plugin(acfactory, lp): ac2.add_account_plugin(FFIEventLogger(ac2)) lp.sec("creating bot test group with bot") - bot_contact = ac1.create_contact(botproc.addr) + bot_chat = ac1.qr_setup_contact(botproc.qr) + ac1._evtracker.wait_securejoin_joiner_progress(1000) + bot_contact = bot_chat.get_contacts()[0] ch = ac1.create_group_chat("bot test group") ch.add_contact(bot_contact) ch.send_text("hello") @@ -60,7 +62,7 @@ def test_group_tracking_plugin(acfactory, lp): ) lp.sec("adding third member {}".format(ac2.get_config("addr"))) - contact3 = ac1.create_contact(ac2.get_config("addr")) + contact3 = ac1.create_contact(ac2) ch.add_contact(contact3) reply = ac1._evtracker.wait_next_incoming_message() diff --git a/python/pyproject.toml b/python/pyproject.toml index 0c3a81e1d9..aa5b360aea 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat" -version = "1.155.2" +version = "1.159.5" description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat" readme = "README.rst" requires-python = ">=3.8" @@ -29,8 +29,8 @@ dependencies = [ ] [project.urls] -"Home" = "https://github.com/deltachat/deltachat-core-rust/" -"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues" +"Home" = "https://github.com/chatmail/core/" +"Bug Tracker" = "https://github.com/chatmail/core/issues" "Documentation" = "https://py.delta.chat/" "Mastodon" = "https://chaos.social/@delta" diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index 3d2734cca5..1d9080b36a 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -55,6 +55,8 @@ def run_cmdline(argv=None, account_plugins=None): args = parser.parse_args(argv[1:]) ac = Account(args.db) + qr = ac.get_setup_contact_qr() + print(qr) ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi) diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index f780139620..8bc199ce9e 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -280,6 +280,12 @@ def create_contact(self, obj, name: Optional[str] = None) -> Contact: :param name: (optional) display name for this contact :returns: :class:`deltachat.contact.Contact` instance. """ + if isinstance(obj, Account): + if not obj.is_configured(): + raise ValueError("Can only add configured accounts as contacts") + assert name is None + vcard = obj.get_self_contact().make_vcard() + return self.import_vcard(vcard)[0] (name, addr) = self.get_contact_addr_and_name(obj, name) name_c = as_dc_charpointer(name) addr_c = as_dc_charpointer(addr) @@ -349,25 +355,26 @@ def get_contacts( self, query: Optional[str] = None, with_self: bool = False, - only_verified: bool = False, ) -> List[Contact]: """get a (filtered) list of contacts. :param query: if a string is specified, only return contacts whose name or e-mail matches query. - :param only_verified: if true only return verified contacts. :param with_self: if true the self-contact is also returned. :returns: list of :class:`deltachat.contact.Contact` objects. """ flags = 0 query_c = as_dc_charpointer(query) - if only_verified: - flags |= const.DC_GCL_VERIFIED_ONLY if with_self: flags |= const.DC_GCL_ADD_SELF dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref) return list(iter_array(dc_array, lambda x: Contact(self, x))) + def import_vcard(self, vcard): + """Import a vCard and return an array of contacts.""" + dc_array = ffi.gc(lib.dc_import_vcard(self._dc_context, as_dc_charpointer(vcard)), lib.dc_array_unref) + return list(iter_array(dc_array, lambda x: Contact(self, x))) + def get_fresh_messages(self) -> Generator[Message, None, None]: """yield all fresh messages from all chats.""" dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref) diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index e28a31178d..fc5713d61a 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -90,6 +90,14 @@ def get_profile_image(self) -> Optional[str]: dc_res = lib.dc_contact_get_profile_image(self._dc_contact) return from_optional_dc_charpointer(dc_res) + def make_vcard(self) -> str: + """Make a contact vCard. + + :returns: vCard + """ + dc_context = self.account._dc_context + return from_dc_charpointer(lib.dc_make_vcard(dc_context, self.id)) + @property def status(self): """Get contact status. diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 3508e2dd7d..d5447d76cd 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -118,7 +118,7 @@ def set_file(self, path, mime_type=None): mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type) if not os.path.exists(path): raise ValueError(f"path does not exist: {path!r}") - lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype) + lib.dc_msg_set_file_and_deduplicate(self._dc_msg, as_dc_charpointer(path), ffi.NULL, mtype) @props.with_doc def basename(self) -> str: @@ -215,7 +215,7 @@ def continue_key_transfer(self, setup_code): """extract key and use it as primary key for this account.""" res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code)) if res == 0: - raise ValueError("could not decrypt") + raise ValueError("Importing the key from Autocrypt Setup Message failed") @props.with_doc def time_sent(self): @@ -285,23 +285,6 @@ def force_plaintext(self) -> None: """Force the message to be sent in plain text.""" lib.dc_msg_force_plaintext(self._dc_msg) - def get_mime_headers(self): - """return mime-header object for an incoming message. - - This only returns a non-None object if ``save_mime_headers`` - config option was set and the message is incoming. - - :returns: email-mime message object (with headers only, no body). - """ - import email - - mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id) - if mime_headers: - s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) - if isinstance(s, bytes): - return email.message_from_bytes(s) - return email.message_from_string(s) - @property def error(self) -> Optional[str]: """Error message.""" diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 92da00ea54..aecd333ff6 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -423,8 +423,6 @@ def get_next_liveconfig(self): where we can make valid SMTP and IMAP connections with. """ configdict = next(self._liveconfig_producer).copy() - if "e2ee_enabled" not in configdict: - configdict["e2ee_enabled"] = "1" if self.pytestconfig.getoption("--strict-tls"): # Enable strict certificate checks for online accounts @@ -484,12 +482,8 @@ def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Acc addr = f"{acname}@offline.org" ac.update_config( { - "addr": addr, - "displayname": acname, - "mail_pw": "123", "configured_addr": addr, - "configured_mail_pw": "123", - "configured": "1", + "displayname": acname, }, ) self._preconfigure_key(ac) @@ -651,6 +645,9 @@ class BotProcess: def __init__(self, popen, addr) -> None: self.popen = popen + + # The first thing the bot prints to stdout is an invite link. + self.qr = self.popen.stdout.readline() self.addr = addr # we read stdout as quickly as we can in a thread and make diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 2366e90adb..0a347e18eb 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -1,7 +1,6 @@ import sys import time -import pytest import deltachat as dc @@ -17,8 +16,6 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp): lp.sec("ac1: send message to new group chat") msg1 = chat.send_text("hello") assert msg1.is_encrypted() - gossiped_timestamp = chat.get_summary()["gossiped_timestamp"] - assert gossiped_timestamp > 0 assert chat.num_contacts() == 3 + 1 @@ -47,19 +44,13 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp): assert to_remove.addr in sysmsg.text assert sysmsg.chat.num_contacts() == 3 - # Receiving message about removed contact does not reset gossip - assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp - lp.sec("ac1: sending another message to the chat") chat.send_text("hello2") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello2" - assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp lp.sec("ac1: adding fifth member to the chat") chat.add_contact(ac5) - # Adding contact to chat resets gossiped_timestamp - assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp lp.sec("ac2: receiving system message about contact addition") sysmsg = ac2._evtracker.wait_next_incoming_message() @@ -196,118 +187,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_encrypted() -@pytest.mark.parametrize("mvbox_move", [False, True]) -def test_fetch_existing(acfactory, lp, mvbox_move): - """Delta Chat reads the recipients from old emails sent by the user and adds them as contacts. - This way, we can already offer them some email addresses they can write to. - - Also, the newest existing emails from each folder are fetched during onboarding. - - Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read.""" - - def assert_folders_configured(ac): - """There was a bug that scan_folders() set the configured folders to None under some circumstances. - So, check that they are still configured:""" - assert ac.get_config("configured_sentbox_folder") == "Sent" - if mvbox_move: - assert ac.get_config("configured_mvbox_folder") - - ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move) - ac2 = acfactory.new_online_configuring_account() - acfactory.wait_configured(ac1) - ac1.direct_imap.create_folder("Sent") - ac1.set_config("sentbox_watch", "1") - - # We need to reconfigure to find the new "Sent" folder. - # `scan_folders()`, which runs automatically shortly after `start_io()` is invoked, - # would also find the "Sent" folder, but it would be too late: - # The sentbox thread, started by `start_io()`, would have seen that there is no - # ConfiguredSentboxFolder and do nothing. - acfactory._acsetup.start_configure(ac1) - acfactory.bring_accounts_online() - assert_folders_configured(ac1) - - lp.sec("send out message with bcc to ourselves") - ac1.set_config("bcc_self", "1") - chat = acfactory.get_accepted_chat(ac1, ac2) - chat.send_text("message text") - - lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen") - if mvbox_move: - ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.") - else: - ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.") - assert_folders_configured(ac1) - - lp.sec("create a cloned ac1 and fetch contact history during configure") - ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1) - ac1_clone.set_config("fetch_existing_msgs", "1") - acfactory.wait_configured(ac1_clone) - ac1_clone.start_io() - assert_folders_configured(ac1_clone) - - lp.sec("check that ac2 contact was fetched during configure") - ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED") - ac2_addr = ac2.get_config("addr") - assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts()) - assert_folders_configured(ac1_clone) - - lp.sec("check that messages changed events arrive for the correct message") - msg = ac1_clone._evtracker.wait_next_messages_changed() - assert msg.text == "message text" - assert_folders_configured(ac1) - assert_folders_configured(ac1_clone) - - -def test_fetch_existing_msgs_group_and_single(acfactory, lp): - """There was a bug concerning fetch-existing-msgs: - - A sent a message to you, adding you to a group. This created a contact request. - You wrote a message to A, creating a chat. - ...but the group stayed blocked. - So, after fetch-existing-msgs you have one contact request and one chat with the same person. - - See https://github.com/deltachat/deltachat-core-rust/issues/2097""" - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account() - - acfactory.bring_accounts_online() - - lp.sec("receive a message") - ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message") - ac1._evtracker.wait_next_incoming_message() - - lp.sec("send out message with bcc to ourselves") - ac1.set_config("bcc_self", "1") - ac1_ac2_chat = ac1.create_chat(ac2) - ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat") - - # wait until the bcc_self message arrives - ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.") - - lp.sec("Clone online account and let it fetch the existing messages") - ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1) - ac1_clone.set_config("fetch_existing_msgs", "1") - acfactory.wait_configured(ac1_clone) - - ac1_clone.start_io() - ac1_clone._evtracker.wait_idle_inbox_ready() - - chats = ac1_clone.get_chats() - assert len(chats) == 4 # two newly created chats + self-chat + device-chat - group_chat = [c for c in chats if c.get_name() == "group name"][0] - assert group_chat.is_group() - (private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()] - assert not private_chat.is_group() - - group_messages = group_chat.get_messages() - assert len(group_messages) == 1 - assert group_messages[0].text == "incoming, unencrypted group message" - private_messages = private_chat.get_messages() - # We can't decrypt the message in this chat, so the chat is empty: - assert len(private_messages) == 0 - - def test_undecipherable_group(acfactory, lp): """Test how group messages that cannot be decrypted are handled. @@ -444,63 +323,6 @@ def test_ephemeral_timer(acfactory, lp): assert chat1.get_ephemeral_timer() == 0 -def test_multidevice_sync_seen(acfactory, lp): - """Test that message marked as seen on one device is marked as seen on another.""" - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account() - ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1) - acfactory.bring_accounts_online() - - ac1.set_config("bcc_self", "1") - ac1_clone.set_config("bcc_self", "1") - - ac1_chat = ac1.create_chat(ac2) - ac1_clone_chat = ac1_clone.create_chat(ac2) - ac2_chat = ac2.create_chat(ac1) - - lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'") - ac2_chat.send_text("Hi") - ac1_message = ac1._evtracker.wait_next_incoming_message() - ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message() - assert ac1_chat.count_fresh_messages() == 1 - assert ac1_clone_chat.count_fresh_messages() == 1 - assert ac1_message.is_in_fresh - assert ac1_clone_message.is_in_fresh - - lp.sec("ac1 marks message as seen on the first device") - ac1.mark_seen_messages([ac1_message]) - assert ac1_message.is_in_seen - - lp.sec("ac1 clone detects that message is marked as seen") - ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED") - assert ev.data1 == ac1_clone_chat.id - assert ac1_clone_message.is_in_seen - - lp.sec("Send an ephemeral message from ac2 to ac1") - ac2_chat.set_ephemeral_timer(60) - ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED") - ac1._evtracker.wait_next_incoming_message() - ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED") - ac1_clone._evtracker.wait_next_incoming_message() - - ac2_chat.send_text("Foobar") - ac1_message = ac1._evtracker.wait_next_incoming_message() - ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message() - assert "Ephemeral timer: 60\n" in ac1_message.get_message_info() - assert "Expires: " not in ac1_clone_message.get_message_info() - assert "Ephemeral timer: 60\n" in ac1_message.get_message_info() - assert "Expires: " not in ac1_clone_message.get_message_info() - - ac1.mark_seen_messages([ac1_message]) - assert ac1_message.is_in_seen - assert "Expires: " in ac1_message.get_message_info() - ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED") - assert ev.data1 == ac1_clone_chat.id - assert ac1_clone_message.is_in_seen - # Test that the timer is started on the second device after synchronizing the seen status. - assert "Expires: " in ac1_clone_message.get_message_info() - - def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): """The test for the bug #3836: - Alice has two devices, the second is offline. @@ -510,6 +332,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): """ ac1, ac2 = acfactory.get_online_accounts(2) ac2_addr = ac2.get_config("addr") + acfactory.remove_preconfigured_keys() ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1) for ac in [ac1, ac1_offl]: ac.set_config("bcc_self", "1") @@ -560,6 +383,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp missing, cannot encrypt". """ ac1, ac2 = acfactory.get_online_accounts(2) + acfactory.remove_preconfigured_keys() ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2) for ac in [ac2, ac2_offl]: ac.set_config("bcc_self", "1") @@ -615,6 +439,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): - Now the seconds device has all members verified. """ ac1, ac2 = acfactory.get_online_accounts(2) + acfactory.remove_preconfigured_keys() ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2) for ac in [ac2, ac2_offl]: ac.set_config("bcc_self", "1") diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 53124c4d13..e75f36a869 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -31,37 +31,6 @@ def test_basic_imap_api(acfactory, tmp_path): imap2.shutdown() -@pytest.mark.ignored() -def test_configure_generate_key(acfactory, lp): - # A slow test which will generate new keys. - acfactory.remove_preconfigured_keys() - ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048)) - ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519)) - acfactory.bring_accounts_online() - chat = acfactory.get_accepted_chat(ac1, ac2) - - 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() - 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() - 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() - assert msg3_in.text == "message3" - assert msg3_in.is_encrypted() - - def test_configure_canceled(acfactory): ac1 = acfactory.new_online_configuring_account() ac1.stop_ongoing() @@ -85,78 +54,6 @@ def test_configure_unref(tmp_path): lib.dc_context_unref(dc_context) -def test_export_import_self_keys(acfactory, tmp_path, lp): - ac1, ac2 = acfactory.get_online_accounts(2) - - dir = tmp_path / "exportdir" - dir.mkdir() - export_files = ac1.export_self_keys(str(dir)) - assert len(export_files) == 2 - for x in export_files: - assert x.startswith(str(dir)) - (key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*") - ac1._evtracker.consume_events() - - lp.sec("exported keys (private and public)") - for name in dir.iterdir(): - lp.indent(str(dir / name)) - lp.sec("importing into existing account") - ac2.import_self_keys(str(dir)) - (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*") - assert key_id2 == key_id - - -def test_one_account_send_bcc_setting(acfactory, lp): - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account() - ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1) - acfactory.bring_accounts_online() - - # test if sent messages are copied to it via BCC. - - chat = acfactory.get_accepted_chat(ac1, ac2) - self_addr = ac1.get_config("addr") - other_addr = ac2.get_config("addr") - - lp.sec("send out message without bcc to ourselves") - ac1.set_config("bcc_self", "0") - msg_out = chat.send_text("message1") - assert not msg_out.is_forwarded() - - # wait for send out (no BCC) - ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT") - assert ac1.get_config("bcc_self") == "0" - - # make sure we are not sending message to ourselves - assert self_addr not in ev.data2 - assert other_addr in ev.data2 - - lp.sec("ac1: setting bcc_self=1") - ac1.set_config("bcc_self", "1") - - lp.sec("send out message with bcc to ourselves") - msg_out = chat.send_text("message2") - - # wait for send out (BCC) - ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT") - assert ac1.get_config("bcc_self") == "1" - - # Second client receives only second message, but not the first. - ev_msg = ac1_clone._evtracker.wait_next_messages_changed() - assert ev_msg.text == msg_out.text - - # now make sure we are sending message to ourselves too - assert self_addr in ev.data2 - assert other_addr in ev.data2 - - # BCC-self messages are marked as seen by the sender device. - ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.") - - # Check that the message is marked as seen on IMAP. - ac1.direct_imap.select_folder("Inbox") - assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1 - - def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) @@ -539,26 +436,6 @@ def test_forward_messages(acfactory, lp): assert not chat3.get_messages() -def test_forward_encrypted_to_unencrypted(acfactory, lp): - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - chat = acfactory.get_protected_chat(ac1, ac2) - - lp.sec("ac1: send encrypted message to ac2") - txt = "This should be encrypted" - chat.send_text(txt) - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.text == txt - assert msg.is_encrypted() - - lp.sec("ac2: forward message to ac3 unencrypted") - unencrypted_chat = ac2.create_chat(ac3) - msg_id = msg.id - msg2 = unencrypted_chat.send_msg(msg) - assert msg2 == msg - assert msg.id != msg_id - assert not msg.is_encrypted() - - def test_forward_own_message(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) @@ -921,12 +798,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp): msg3.mark_seen() assert not list(ac1.get_fresh_messages()) - # Test that we do not gossip peer keys in 1-to-1 chat, - # as it makes no sense to gossip to peers their own keys. - # Gossip is only sent in encrypted messages, - # and we sent encrypted msg_back right above. - assert chat2b.get_summary()["gossiped_timestamp"] == 0 - lp.sec("create group chat with two members, one of which has no encrypt state") chat = ac1.create_group_chat("encryption test") chat.add_contact(ac2) @@ -936,102 +807,8 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp): ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT") -def test_gossip_optimization(acfactory, lp): - """Test that gossip timestamp is updated when someone else sends gossip, - so we don't have to send gossip ourselves. - """ - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - - acfactory.introduce_each_other([ac1, ac2]) - acfactory.introduce_each_other([ac2, ac3]) - - lp.sec("ac1 creates a group chat with ac2") - group_chat = ac1.create_group_chat("hello") - group_chat.add_contact(ac2) - msg = group_chat.send_text("hi") - - # No Autocrypt gossip was sent yet. - gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"] - assert gossiped_timestamp == 0 - - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.is_encrypted() - assert msg.text == "hi" - - lp.sec("ac2 adds ac3 to the group") - msg.chat.add_contact(ac3) - - lp.sec("ac1 receives message from ac2 and updates gossip timestamp") - msg = ac1._evtracker.wait_next_incoming_message() - assert msg.is_encrypted() - - # ac1 has updated the gossip timestamp even though no gossip was sent by ac1. - # ac1 does not need to send gossip because ac2 already did it. - gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"] - assert gossiped_timestamp == int(msg.time_sent.timestamp()) - - -def test_gossip_encryption_preference(acfactory, lp): - """Test that encryption preference of group members is gossiped to new members. - This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers - SHOULD NOT contain encryption preference. - """ - ac1, ac2, ac3 = acfactory.get_online_accounts(3) - - lp.sec("ac1 learns that ac2 prefers encryption") - ac1.create_chat(ac2) - msg = ac2.create_chat(ac1).send_text("first message") - msg = ac1._evtracker.wait_next_incoming_message() - assert msg.text == "first message" - assert not msg.is_encrypted() - res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr")) - assert msg.chat.get_encryption_info() == res - lp.sec("ac2 learns that ac3 prefers encryption") - ac2.create_chat(ac3) - msg = ac3.create_chat(ac2).send_text("I prefer encryption") - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.text == "I prefer encryption" - assert not msg.is_encrypted() - - lp.sec("ac3 does not know that ac1 prefers encryption") - ac1.create_chat(ac3) - chat = ac3.create_chat(ac1) - res = "No encryption:\n{}".format(ac1.get_config("addr")) - assert chat.get_encryption_info() == res - msg = chat.send_text("not encrypted") - msg = ac1._evtracker.wait_next_incoming_message() - assert msg.text == "not encrypted" - assert not msg.is_encrypted() - - lp.sec("ac1 creates a group chat with ac2") - group_chat = ac1.create_group_chat("hello") - group_chat.add_contact(ac2) - encryption_info = group_chat.get_encryption_info() - res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr")) - assert encryption_info == res - msg = group_chat.send_text("hi") - - msg = ac2._evtracker.wait_next_incoming_message() - assert msg.is_encrypted() - assert msg.text == "hi" - - lp.sec("ac2 adds ac3 to the group") - msg.chat.add_contact(ac3) - assert msg.is_encrypted() - - lp.sec("ac3 learns that ac1 prefers encryption") - msg = ac3._evtracker.wait_next_incoming_message() - encryption_info = msg.chat.get_encryption_info().splitlines() - assert encryption_info[0] == "End-to-end encryption preferred:" - assert ac1.get_config("addr") in encryption_info[1:] - assert ac2.get_config("addr") in encryption_info[1:] - msg = chat.send_text("encrypted") - assert msg.is_encrypted() - - def test_send_first_message_as_long_unicode_with_cr(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) - ac2.set_config("save_mime_headers", "1") lp.sec("ac1: create chat with ac2") chat = acfactory.get_accepted_chat(ac1, ac2) @@ -1042,7 +819,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp): " wrapped using format=flowed and unwrapped on the receiver" ) msg_out = chat.send_text(text1) - assert not msg_out.is_encrypted() + assert msg_out.is_encrypted() lp.sec("wait for ac2 to receive multi-line non-unicode message") msg_in = ac2._evtracker.wait_next_incoming_message() @@ -1051,7 +828,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp): lp.sec("sending multi-line unicode text message from ac1 to ac2") text2 = "äalis\nthis is ßßÄ" msg_out = chat.send_text(text2) - assert not msg_out.is_encrypted() + assert msg_out.is_encrypted() lp.sec("wait for ac2 to receive multi-line unicode message") msg_in = ac2._evtracker.wait_next_incoming_message() @@ -1220,93 +997,6 @@ def test_dont_show_emails(acfactory, lp): assert len(msg.chat.get_messages()) == 3 -def test_no_old_msg_is_fresh(acfactory, lp): - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account() - ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1) - acfactory.bring_accounts_online() - - ac1.set_config("e2ee_enabled", "0") - ac1_clone.set_config("e2ee_enabled", "0") - ac2.set_config("e2ee_enabled", "0") - - ac1_clone.set_config("bcc_self", "1") - - ac1.create_chat(ac2) - ac1_clone.create_chat(ac2) - - ac1.get_device_chat().mark_noticed() - - lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'") - first_msg_id = ac2.create_chat(ac1).send_text("Hi") - ac1._evtracker.wait_next_incoming_message() - assert ac1.create_chat(ac2).count_fresh_messages() == 1 - assert len(list(ac1.get_fresh_messages())) == 1 - - lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'") - ac1_clone.create_chat(ac2).send_text("Hi back") - ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED") - - assert ev.data1 == first_msg_id.chat.id - assert ac1.create_chat(ac2).count_fresh_messages() == 0 - assert len(list(ac1.get_fresh_messages())) == 0 - - -def test_prefer_encrypt(acfactory, lp): - """Test quorum rule for encryption preference in 1:1 and group chat.""" - ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True) - ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True) - ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True) - acfactory.bring_accounts_online() - ac1.set_config("e2ee_enabled", "0") - ac2.set_config("e2ee_enabled", "1") - ac3.set_config("e2ee_enabled", "0") - - # Make sure we do not send a copy to ourselves. This is to - # test that we count own preference even when we are not in - # the recipient list. - ac1.set_config("bcc_self", "0") - ac2.set_config("bcc_self", "0") - ac3.set_config("bcc_self", "0") - - acfactory.introduce_each_other([ac1, ac2, ac3]) - - lp.sec("ac1: sending message to ac2") - chat1 = ac1.create_chat(ac2) - msg1 = chat1.send_text("message1") - assert not msg1.is_encrypted() - ac2._evtracker.wait_next_incoming_message() - - lp.sec("ac2: sending message to ac1") - chat2 = ac2.create_chat(ac1) - msg2 = chat2.send_text("message2") - # Own preference is `Mutual` and we have the peer's key. - assert msg2.is_encrypted() - ac1._evtracker.wait_next_incoming_message() - - lp.sec("ac1: sending message to group chat with ac2 and ac3") - group = ac1.create_group_chat("hello") - group.add_contact(ac2) - group.add_contact(ac3) - msg3 = group.send_text("message3") - assert not msg3.is_encrypted() - ac2._evtracker.wait_next_incoming_message() - ac3._evtracker.wait_next_incoming_message() - - lp.sec("ac3: start preferring encryption and inform ac1") - ac3.set_config("e2ee_enabled", "1") - chat3 = ac3.create_chat(ac1) - msg4 = chat3.send_text("message4") - # Own preference is `Mutual` and we have the peer's key. - assert msg4.is_encrypted() - ac1._evtracker.wait_next_incoming_message() - - lp.sec("ac1: sending another message to group chat with ac2 and ac3") - msg5 = group.send_text("message5") - # Majority prefers encryption now - assert msg5.is_encrypted() - - def test_bot(acfactory, lp): """Test that bot messages can be identified as such""" ac1, ac2 = acfactory.get_online_accounts(2) @@ -1335,59 +1025,6 @@ def test_bot(acfactory, lp): assert msg_in.is_bot() -def test_quote_encrypted(acfactory, lp): - """Test that replies to encrypted messages with quotes are encrypted.""" - ac1, ac2 = acfactory.get_online_accounts(2) - - lp.sec("ac1: create chat with ac2") - chat = ac1.create_chat(ac2) - - lp.sec("sending text message from ac1 to ac2") - msg1 = chat.send_text("message1") - assert not msg1.is_encrypted() - - lp.sec("wait for ac2 to receive message") - msg2 = ac2._evtracker.wait_next_incoming_message() - assert msg2.text == "message1" - assert not msg2.is_encrypted() - - lp.sec("create new chat with contact and send back (encrypted) message") - msg2.create_chat().send_text("message-back") - - lp.sec("wait for ac1 to receive message") - msg3 = ac1._evtracker.wait_next_incoming_message() - assert msg3.text == "message-back" - assert msg3.is_encrypted() - - lp.sec("ac1: e2ee_enabled=0 and see if reply is encrypted") - print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled"))) - print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled"))) - ac1.set_config("e2ee_enabled", "0") - - for quoted_msg in msg1, msg3: - # Save the draft with a quote. - msg_draft = Message.new_empty(ac1, "text") - msg_draft.set_text("message reply") - msg_draft.quote = quoted_msg - chat.set_draft(msg_draft) - - # Get the draft and send it. - msg_draft = chat.get_draft() - chat.send_msg(msg_draft) - - chat.set_draft(None) - assert chat.get_draft() is None - - # Quote should be replaced with "..." if quoted message is encrypted. - msg_in = ac2._evtracker.wait_next_incoming_message() - assert msg_in.text == "message reply" - assert not msg_in.is_encrypted() - if quoted_msg.is_encrypted(): - assert msg_in.quoted_text == "..." - else: - assert msg_in.quoted_text == quoted_msg.text - - def test_quote_attachment(tmp_path, acfactory, lp): """Test that replies with an attachment and a quote are received correctly.""" ac1, ac2 = acfactory.get_online_accounts(2) @@ -1421,26 +1058,6 @@ def test_quote_attachment(tmp_path, acfactory, lp): assert open(received_reply.filename).read() == "data to send" -def test_saved_mime_on_received_message(acfactory, lp): - ac1, ac2 = acfactory.get_online_accounts(2) - - lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat") - ac2.set_config("save_mime_headers", "1") - chat = ac1.create_chat(ac2) - - lp.sec("sending text message from ac1 to ac2") - msg_out = chat.send_text("message1") - ac1._evtracker.wait_msg_delivered(msg_out) - assert msg_out.get_mime_headers() is None - - lp.sec("wait for ac2 to receive message") - ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG") - in_id = ev.data2 - mime = ac2.get_message_by_id(in_id).get_mime_headers() - assert mime.get_all("From") - assert mime.get_all("Received") - - def test_send_mark_seen_clean_incoming_events(acfactory, lp): ac1, ac2 = acfactory.get_online_accounts(2) chat = acfactory.get_accepted_chat(ac1, ac2) @@ -1597,53 +1214,6 @@ def assert_account_is_proper(ac): assert ac2.get_latest_backupfile(str(backupdir)) == path2 -def test_ac_setup_message(acfactory, lp): - # note that the receiving account needs to be configured and running - # before the setup message is send. DC does not read old messages - # as of Jul2019 - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account(cloned_from=ac1) - acfactory.bring_accounts_online() - - lp.sec("trigger ac setup message and return setupcode") - assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] - setup_code = ac1.initiate_key_transfer() - ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - msg = ac2.get_message_by_id(ev.data2) - assert msg.is_setup_message() - assert msg.get_setupcodebegin() == setup_code[:2] - lp.sec("try a bad setup code") - with pytest.raises(ValueError): - msg.continue_key_transfer(str(reversed(setup_code))) - lp.sec("try a good setup code") - print("*************** Incoming ASM File at: ", msg.filename) - print("*************** Setup Code: ", setup_code) - msg.continue_key_transfer(setup_code) - assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"] - - -def test_ac_setup_message_twice(acfactory, lp): - ac1 = acfactory.new_online_configuring_account() - ac2 = acfactory.new_online_configuring_account(cloned_from=ac1) - acfactory.bring_accounts_online() - - lp.sec("trigger ac setup message but ignore") - assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] - ac1.initiate_key_transfer() - ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - - lp.sec("trigger second ac setup message, wait for receive ") - setup_code2 = ac1.initiate_key_transfer() - ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") - msg = ac2.get_message_by_id(ev.data2) - assert msg.is_setup_message() - assert msg.get_setupcodebegin() == setup_code2[:2] - - lp.sec("process second setup message") - msg.continue_key_transfer(setup_code2) - assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"] - - def test_qr_email_capitalization(acfactory, lp): """Regression test for a bug that resulted in failure to propagate verification via gossip in a verified group @@ -1664,7 +1234,7 @@ def test_qr_email_capitalization(acfactory, lp): lp.sec("ac1 joins a verified group via a QR code") ac1_chat = ac1.qr_join_chat(qr) msg = ac1._evtracker.wait_next_incoming_message() - assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr")) + assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr")) assert len(ac1_chat.get_contacts()) == 2 lp.sec("ac2 joins a verified group via a QR code") @@ -1763,7 +1333,7 @@ def ac_member_removed(self, chat, contact, message): lp.sec("ac1: add address2") # note that if the above create_chat() would not # happen we would not receive a proper member_added event - contact2 = chat.add_contact(ac3_addr) + contact2 = chat.add_contact(ac3) ev = in_list.get() assert ev.action == "chat-modified" ev = in_list.get() @@ -1932,15 +1502,6 @@ def test_connectivity(acfactory, lp): assert len(msgs) == 2 assert msgs[1].text == "Hi 2" - lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong") - - ac1.set_config("configured_mail_pw", "abc") - ac1.stop_io() - ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED) - ac1.start_io() - ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING) - ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED) - def test_fetch_deleted_msg(acfactory, lp): """This is a regression test: Messages with \\Deleted flag were downloaded again and again, @@ -2056,7 +1617,7 @@ def test_immediate_autodelete(acfactory, lp): lp.sec("ac2: wait for close/expunge on autodelete") ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") - ac2._evtracker.get_info_contains("close/expunge succeeded") + ac2._evtracker.get_info_contains("Close/expunge succeeded.") lp.sec("ac2: check that message was autodeleted on server") assert len(ac2.direct_imap.get_all_messages()) == 0 @@ -2092,7 +1653,7 @@ def test_delete_multiple_messages(acfactory, lp): lp.sec("ac2: test that only one message is left") while 1: ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") - ac2._evtracker.get_info_contains("close/expunge succeeded") + ac2._evtracker.get_info_contains("Close/expunge succeeded.") ac2.direct_imap.select_config_folder("inbox") nr_msgs = len(ac2.direct_imap.get_all_messages()) assert nr_msgs > 0 @@ -2203,9 +1764,7 @@ def test_name_changes(acfactory): ac1, ac2 = acfactory.get_online_accounts(2) ac1.set_config("displayname", "Account 1") - # Similar to acfactory.get_accepted_chat, but without setting the contact name. - ac2.create_contact(ac1.get_config("addr")).create_chat() - chat12 = ac1.create_contact(ac2.get_config("addr")).create_chat() + chat12 = acfactory.get_accepted_chat(ac1, ac2) contact = None def update_name(): @@ -2370,23 +1929,6 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination): assert len(ac1.direct_imap.get_all_messages()) == 0 -def test_delete_deltachat_folder(acfactory): - """Test that DeltaChat folder is recreated if user deletes it manually.""" - ac1 = acfactory.new_online_configuring_account(mvbox_move=True) - ac2 = acfactory.new_online_configuring_account() - acfactory.wait_configured(ac1) - - ac1.direct_imap.conn.folder.delete("DeltaChat") - assert "DeltaChat" not in ac1.direct_imap.list_folders() - acfactory.bring_accounts_online() - - ac2.create_chat(ac1).send_text("hello") - msg = ac1._evtracker.wait_next_incoming_message() - assert msg.text == "hello" - - assert "DeltaChat" in ac1.direct_imap.list_folders() - - def test_archived_muted_chat(acfactory, lp): """If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously. diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index cb5d2ac77f..aae9582163 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -105,10 +105,6 @@ def test_update_config(self, acfactory): ac1.update_config({"mvbox_move": False}) assert ac1.get_config("mvbox_move") == "0" - def test_has_savemime(self, acfactory): - ac1 = acfactory.get_unconfigured_account() - assert "save_mime_headers" in ac1.get_config("sys.config_keys").split() - def test_has_bccself(self, acfactory): ac1 = acfactory.get_unconfigured_account() assert "bcc_self" in ac1.get_config("sys.config_keys").split() @@ -188,7 +184,6 @@ def test_get_contacts_and_delete(self, acfactory): assert not ac1.get_contacts(query="some2") assert ac1.get_contacts(query="some1") - assert not ac1.get_contacts(only_verified=True) assert len(ac1.get_contacts(with_self=True)) == 2 assert ac1.delete_contact(contact1) @@ -436,11 +431,11 @@ def test_message_file(self, chat1, data, lp, fn, typein, typeout): assert msg.id > 0 assert msg.is_file() assert os.path.exists(msg.filename) - assert msg.filename.endswith(msg.basename) + assert msg.filename.endswith(".txt") == fn.endswith(".txt") assert msg.filemime == typeout msg2 = chat1.send_file(fp, typein) assert msg2 != msg - assert msg2.filename != msg.filename + assert msg2.filename == msg.filename def test_create_contact(self, acfactory): ac1 = acfactory.get_pseudo_configured_account() diff --git a/python/tox.ini b/python/tox.ini index 9866e3b5b9..64a693bbca 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -45,7 +45,7 @@ deps = pygments restructuredtext_lint commands = - ruff format --quiet --diff setup.py src/deltachat examples/ tests/ + ruff format --diff setup.py src/deltachat examples/ tests/ ruff check src/deltachat tests/ examples/ rst-lint --encoding 'utf-8' README.rst diff --git a/release-date.in b/release-date.in index 2da08eae1a..47a3b03014 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2025-01-31 \ No newline at end of file +2025-05-14 \ No newline at end of file diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml index 73e2d7469b..20c099a4a1 100644 --- a/scripts/concourse/docs_wheels.yml +++ b/scripts/concourse/docs_wheels.yml @@ -4,14 +4,14 @@ resources: icon: github source: branch: main - uri: https://github.com/deltachat/deltachat-core-rust.git + uri: https://github.com/chatmail/core.git - name: deltachat-core-rust-release type: git icon: github source: branch: main - uri: https://github.com/deltachat/deltachat-core-rust.git + uri: https://github.com/chatmail/core.git tag_filter: "v*" jobs: diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh index 844b8442a4..1919e3c0c1 100755 --- a/scripts/coredeps/install-rust.sh +++ b/scripts/coredeps/install-rust.sh @@ -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.84.0 +RUST_VERSION=1.87.0 ARCH="$(uname -m)" test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu diff --git a/scripts/create-provider-data-rs.py b/scripts/create-provider-data-rs.py index 8e68910de2..62c25b1b91 100755 --- a/scripts/create-provider-data-rs.py +++ b/scripts/create-provider-data-rs.py @@ -215,7 +215,7 @@ def process_dir(dir): " Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n" "};\n" "use std::collections::HashMap;\n\n" - "use once_cell::sync::Lazy;\n\n" + "use std::sync::LazyLock;\n\n" ) process_dir(Path(sys.argv[1])) @@ -224,7 +224,7 @@ def process_dir(dir): out_all += out_domains out_all += "];\n\n" - out_all += "pub(crate) static PROVIDER_IDS: Lazy> = Lazy::new(|| HashMap::from([\n" + out_all += "pub(crate) static PROVIDER_IDS: LazyLock> = LazyLock::new(|| HashMap::from([\n" out_all += out_ids out_all += "]));\n\n" @@ -233,8 +233,8 @@ def process_dir(dir): else: now = datetime.datetime.fromisoformat(sys.argv[2]) out_all += ( - "pub static _PROVIDER_UPDATED: Lazy = " - "Lazy::new(|| chrono::NaiveDate::from_ymd_opt(" + "pub static _PROVIDER_UPDATED: LazyLock = " + "LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(" + str(now.year) + ", " + str(now.month) diff --git a/scripts/set_core_version.py b/scripts/set_core_version.py index d2abbdb3b1..f140833df6 100755 --- a/scripts/set_core_version.py +++ b/scripts/set_core_version.py @@ -67,7 +67,6 @@ def main(): parser.add_argument("newversion") json_list = [ - "package.json", "deltachat-jsonrpc/typescript/package.json", "deltachat-rpc-server/npm-package/package.json", ] diff --git a/scripts/wheel-rpc-server.py b/scripts/wheel-rpc-server.py index 6ac1ee6219..4118ec4118 100755 --- a/scripts/wheel-rpc-server.py +++ b/scripts/wheel-rpc-server.py @@ -154,6 +154,8 @@ def main(): "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", + "arm64-v8a-android": "android_21_arm64_v8a", + "armeabi-v7a-android": "android_21_armeabi_v7a", "win64": "win_amd64", "win32": "win32", # macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html diff --git a/spec.md b/spec.md index 080cc8d668..2d8a8fc8f6 100644 --- a/spec.md +++ b/spec.md @@ -1,10 +1,10 @@ -# chat-mail specification +# Chatmail Specification -Version: 0.35.0 +Version: 0.36.0 Status: In-progress Format: [Semantic Line Breaks](https://sembr.org/) -This document roughly describes how chat-mail +This document roughly describes how chatmail apps use the standard e-mail system to implement typical messenger functions. @@ -18,6 +18,8 @@ to implement typical messenger functions. - [Add and remove members](#add-and-remove-members) - [Change group name](#change-group-name) - [Set group image](#set-group-image) +- [Request editing](#request-editing) +- [Request deletion](#request-deletion) - [Set profile image](#set-profile-image) - [Locations](#locations) - [User locations](#user-locations) @@ -304,6 +306,84 @@ To save data, it is RECOMMENDED to add a `Chat-Group-Avatar` only on image changes. +# Request editing + +To request recipients to edit the text of an already sent message, +the messenger MUST set the header `Chat-Edit` +with value set to the message-id of the message to edit +and the body to the new message text. + +The body MAY be prefixed by a quote +and the emoji "✏️" directly before the new text. +Both MUST be skipped by the recipient. + +Receiving messengers MUST look up the message-id from `Chat-Edit`, +replace the text and MAY indicate the edit in the UI. + +The new message text MUST NOT be empty. +It is not possible to edit images or other attachments, including HTML messages. +However, they can be deleted for everyone. + +Example: + + From: sender@domain + To: rcpt@domain + Chat-Version: 1.0 + Message-ID: 00001@domain + Content-Type: text/plain + + Hello wordl! + +The typo from the message above can be fixed by the following message: + + From: sender@domain + To: rcpt@domain + Chat-Version: 1.0 + Chat-Edit: 00001@domain + In-Reply-To: 00001@domain + Message-ID: 00002@domain + Content-Type: text/plain + + On 2025-03-27, sender@domain wrote: + > Hello wordl! + + ✏️Hello world! + + +# Request deletion + +To request recipient to delete a message, +the messenger MUST set the header `Chat-Delete` +with the value set to the message-id of the message to delete. + +Receiving messengers MUST look up the message-id, delete the corresponding message +and MAY indicating the deletion in the UI. + +The sender MUST set the body to any, non-empty text. +The receiver MUST ignore the body. + +Example: + + From: sender@domain + To: rcpt@domain + Chat-Version: 1.0 + Message-ID: 00003@domain + Content-Type: text/plain + + reminder for my pin: 1234 + +The message above can be requested for deletion by the following message: + + From: sender@domain + To: rcpt@domain + Chat-Version: 1.0 + Chat-Delete: 00003@domain + Message-ID: 00004@domain + Content-Type: text/plain + + foo + + # Set profile image A user MAY have a profile-image that MAY be distributed to their contacts. @@ -375,7 +455,7 @@ eg. forwarded from a normal MUA. - + 2020-01-11T20:40:19Z 1.234,5.678 @@ -542,4 +622,4 @@ We define the effective date of a message as the sending time of the message as indicated by its Date header, or the time of first receipt if that date is in the future or unavailable. -Copyright © 2017-2021 Delta Chat contributors. +Copyright © Chatmail contributors. diff --git a/src/accounts.rs b/src/accounts.rs index 4d18316b43..3745cfca1b 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -4,13 +4,11 @@ use std::collections::BTreeMap; use std::future::Future; use std::path::{Path, PathBuf}; -use anyhow::{ensure, Context as _, Result}; -use futures::stream::FuturesUnordered; -use futures::StreamExt; +use anyhow::{bail, ensure, Context as _, Result}; use serde::{Deserialize, Serialize}; use tokio::fs; use tokio::io::AsyncWriteExt; -use tokio::task::JoinHandle; +use tokio::task::{JoinHandle, JoinSet}; use uuid::Uuid; #[cfg(not(target_os = "ios"))] @@ -73,9 +71,7 @@ impl Accounts { let config_file = dir.join(CONFIG_NAME); ensure!(config_file.exists(), "{:?} does not exist", config_file); - let config = Config::from_file(config_file, writable) - .await - .context("failed to load accounts config")?; + let config = Config::from_file(config_file, writable).await?; let events = Events::new(); let stockstrings = StockStrings::new(); let push_subscriber = PushSubscriber::new(); @@ -306,12 +302,6 @@ impl Accounts { /// This is an auxiliary function and not part of public API. /// Use [Accounts::background_fetch] instead. async fn background_fetch_no_timeout(accounts: Vec, events: Events) { - async fn background_fetch_and_log_error(account: Context) { - if let Err(error) = account.background_fetch().await { - warn!(account, "{error:#}"); - } - } - events.emit(Event { id: 0, typ: EventType::Info(format!( @@ -319,11 +309,15 @@ impl Accounts { accounts.len() )), }); - let mut futures_unordered: FuturesUnordered<_> = accounts - .into_iter() - .map(background_fetch_and_log_error) - .collect(); - while futures_unordered.next().await.is_some() {} + let mut set = JoinSet::new(); + for account in accounts { + set.spawn(async move { + if let Err(error) = account.background_fetch().await { + warn!(account, "{error:#}"); + } + }); + } + set.join_all().await; } /// Auxiliary function for [Accounts::background_fetch]. @@ -460,7 +454,9 @@ impl Config { rx.await?; Ok(()) }); - locked_rx.await?; + if locked_rx.await.is_err() { + bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"); + }; Ok(Some(lock_task)) } diff --git a/src/authres.rs b/src/authres.rs index 84504d1ee2..21879b0f4b 100644 --- a/src/authres.rs +++ b/src/authres.rs @@ -4,12 +4,12 @@ use std::borrow::Cow; use std::collections::BTreeSet; use std::fmt; +use std::sync::LazyLock; use anyhow::Result; use deltachat_contact_tools::EmailAddress; use mailparse::MailHeaderMap; use mailparse::ParsedMail; -use once_cell::sync::Lazy; use crate::config::Config; use crate::context::Context; @@ -107,7 +107,8 @@ fn remove_comments(header: &str) -> Cow<'_, str> { // In Pomsky, this is: // "(" Codepoint* lazy ")" // See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22 - static RE: Lazy = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap()); + static RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap()); RE.replace_all(header, " ") } diff --git a/src/blob.rs b/src/blob.rs index 1574956bf8..1589b1d3e9 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1,7 +1,6 @@ //! # Blob directory management. use core::cmp::max; -use std::ffi::OsStr; use std::io::{Cursor, Seek}; use std::iter::FusedIterator; use std::mem; @@ -14,8 +13,7 @@ use image::codecs::jpeg::JpegEncoder; use image::ImageReader; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba}; use num_traits::FromPrimitive; -use tokio::io::AsyncWriteExt; -use tokio::{fs, io, task}; +use tokio::{fs, task}; use tokio_stream::wrappers::ReadDirStream; use crate::config::Config; @@ -23,6 +21,7 @@ use crate::constants::{self, MediaQuality}; use crate::context::Context; use crate::events::EventType; use crate::log::LogExt; +use crate::tools::sanitize_filename; /// Represents a file in the blob directory. /// @@ -47,73 +46,6 @@ enum ImageOutputFormat { } impl<'a> BlobObject<'a> { - /// Creates a new file, returning a tuple of the name and the handle. - async fn create_new_file( - context: &Context, - dir: &Path, - stem: &str, - ext: &str, - ) -> Result<(String, fs::File)> { - const MAX_ATTEMPT: u32 = 16; - let mut attempt = 0; - let mut name = format!("{stem}{ext}"); - loop { - attempt += 1; - let path = dir.join(&name); - match fs::OpenOptions::new() - // Using `create_new(true)` in order to avoid race conditions - // when creating multiple files with the same name. - .create_new(true) - .write(true) - .open(&path) - .await - { - Ok(file) => return Ok((name, file)), - Err(err) => { - if attempt >= MAX_ATTEMPT { - return Err(err).context("failed to create file"); - } else if attempt == 1 && !dir.exists() { - fs::create_dir_all(dir).await.log_err(context).ok(); - } else { - name = format!("{}-{}{}", stem, rand::random::(), ext); - } - } - } - } - } - - /// Creates a new blob object with unique name by copying an existing file. - /// - /// This creates a new blob - /// and copies an existing file into it. This is done in a - /// in way which avoids race-conditions when multiple files are - /// concurrently created. - pub async fn create_and_copy(context: &'a Context, src: &Path) -> Result> { - let mut src_file = fs::File::open(src) - .await - .with_context(|| format!("failed to open file {}", src.display()))?; - let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy()); - let (name, mut dst_file) = - BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?; - let name_for_err = name.clone(); - if let Err(err) = io::copy(&mut src_file, &mut dst_file).await { - // Attempt to remove the failed file, swallow errors resulting from that. - let path = context.get_blobdir().join(&name_for_err); - fs::remove_file(path).await.ok(); - return Err(err).context("failed to copy file"); - } - - // Ensure that all buffered bytes are written - dst_file.flush().await?; - - let blob = BlobObject { - blobdir: context.get_blobdir(), - name: format!("$BLOBDIR/{name}"), - }; - context.emit_event(EventType::NewBlobFile(blob.as_name().to_string())); - Ok(blob) - } - /// Creates a blob object by copying or renaming an existing file. /// If the source file is already in the blobdir, it will be renamed, /// otherwise it will be copied to the blobdir first. @@ -159,10 +91,9 @@ impl<'a> BlobObject<'a> { let hash = hash.get(0..31).unwrap_or(hash); let new_file = if let Some(extension) = original_name.extension().filter(|e| e.len() <= 32) { - format!( - "$BLOBDIR/{hash}.{}", - extension.to_string_lossy().to_lowercase() - ) + let extension = extension.to_string_lossy().to_lowercase(); + let extension = sanitize_filename(&extension); + format!("$BLOBDIR/{hash}.{extension}") } else { format!("$BLOBDIR/{hash}") }; @@ -209,27 +140,6 @@ impl<'a> BlobObject<'a> { }) } - /// Creates a blob from a file, possibly copying it to the blobdir. - /// - /// If the source file is not a path to into the blob directory - /// the file will be copied into the blob directory first. If the - /// source file is already in the blobdir it will not be copied - /// and only be created if it is a valid blobname, that is no - /// subdirectory is used and [BlobObject::sanitise_name] does not - /// modify the filename. - /// - /// Paths into the blob directory may be either defined by an absolute path - /// or by the relative prefix `$BLOBDIR`. - pub async fn new_from_path(context: &'a Context, src: &Path) -> Result> { - if src.starts_with(context.get_blobdir()) { - BlobObject::from_path(context, src) - } else if src.starts_with("$BLOBDIR/") { - BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string()) - } else { - BlobObject::create_and_copy(context, src).await - } - } - /// Returns a [BlobObject] for an existing blob from a path. /// /// The path must designate a file directly in the blobdir and @@ -240,11 +150,11 @@ impl<'a> BlobObject<'a> { let rel_path = path .strip_prefix(context.get_blobdir()) .with_context(|| format!("wrong blobdir: {}", path.display()))?; - if !BlobObject::is_acceptible_blob_name(rel_path) { + let name = rel_path.to_str().context("wrong name")?; + if !BlobObject::is_acceptible_blob_name(name) { return Err(format_err!("bad blob name: {}", rel_path.display())); } - let name = rel_path.to_str().context("wrong name")?; - BlobObject::from_name(context, name.to_string()) + BlobObject::from_name(context, name) } /// Returns a [BlobObject] for an existing blob. @@ -253,13 +163,13 @@ impl<'a> BlobObject<'a> { /// prefixed, as returned by [BlobObject::as_name]. This is how /// you want to create a [BlobObject] for a filename read from the /// database. - pub fn from_name(context: &'a Context, name: String) -> Result> { - let name: String = match name.starts_with("$BLOBDIR/") { - true => name.splitn(2, '/').last().unwrap().to_string(), + pub fn from_name(context: &'a Context, name: &str) -> Result> { + let name = match name.starts_with("$BLOBDIR/") { + true => name.splitn(2, '/').last().unwrap(), false => name, }; - if !BlobObject::is_acceptible_blob_name(&name) { - return Err(format_err!("not an acceptable blob name: {}", &name)); + if !BlobObject::is_acceptible_blob_name(name) { + return Err(format_err!("not an acceptable blob name: {}", name)); } Ok(BlobObject { blobdir: context.get_blobdir(), @@ -283,21 +193,12 @@ impl<'a> BlobObject<'a> { /// Note that this is NOT the user-visible filename, /// which is only stored in Param::Filename on the message. /// + #[allow(rustdoc::private_intra_doc_links)] /// [Params]: crate::param::Params pub fn as_name(&self) -> &str { &self.name } - /// Returns the filename of the blob. - pub fn as_file_name(&self) -> &str { - self.name.rsplit('/').next().unwrap_or_default() - } - - /// The path relative in the blob directory. - pub fn as_rel_path(&self) -> &Path { - Path::new(self.as_file_name()) - } - /// Returns the extension of the blob. /// /// If a blob's filename has an extension, it is always guaranteed @@ -311,94 +212,21 @@ impl<'a> BlobObject<'a> { } } - /// Create a safe name based on a messy input string. - /// - /// The safe name will be a valid filename on Unix and Windows and - /// not contain any path separators. The input can contain path - /// segments separated by either Unix or Windows path separators, - /// the rightmost non-empty segment will be used as name, - /// sanitised for special characters. - /// - /// The resulting name is returned as a tuple, the first part - /// being the stem or basename and the second being an extension, - /// including the dot. E.g. "foo.txt" is returned as `("foo", - /// ".txt")` while "bar" is returned as `("bar", "")`. - /// - /// The extension part will always be lowercased. - fn sanitise_name(name: &str) -> (String, String) { - let mut name = name; - for part in name.rsplit('/') { - if !part.is_empty() { - name = part; - break; - } - } - for part in name.rsplit('\\') { - if !part.is_empty() { - name = part; - break; - } - } - let opts = sanitize_filename::Options { - truncate: true, - windows: true, - replacement: "", - }; - - let name = sanitize_filename::sanitize_with_options(name, opts); - // Let's take a tricky filename, - // "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example. - // Assume that the extension is 32 chars maximum. - let ext: String = name - .chars() - .rev() - .take_while(|c| { - (!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control() - }) - .take(33) - .collect::>() - .iter() - .rev() - .collect(); - // ext == "nd_point_and_double_ending.tar.gz" - - // Split it into "nd_point_and_double_ending" and "tar.gz": - let mut iter = ext.splitn(2, '.'); - iter.next(); - - let ext = iter.next().unwrap_or_default(); - let ext = if ext.is_empty() { - String::new() - } else { - format!(".{ext}") - // ".tar.gz" - }; - let stem = name - .strip_suffix(&ext) - .unwrap_or_default() - .chars() - .take(64) - .collect(); - (stem, ext.to_lowercase()) - } - /// Checks whether a name is a valid blob name. /// /// This is slightly less strict than stanitise_name, presumably /// someone already created a file with such a name so we just - /// ensure it's not actually a path in disguise is actually utf-8. - fn is_acceptible_blob_name(name: impl AsRef) -> bool { - let uname = match name.as_ref().to_str() { - Some(name) => name, - None => return false, - }; - if uname.find('/').is_some() { + /// ensure it's not actually a path in disguise. + /// + /// Acceptible blob name always have to be valid utf-8. + fn is_acceptible_blob_name(name: &str) -> bool { + if name.find('/').is_some() { return false; } - if uname.find('\\').is_some() { + if name.find('\\').is_some() { return false; } - if uname.find('\0').is_some() { + if name.find('\0').is_some() { return false; } true @@ -424,26 +252,30 @@ impl<'a> BlobObject<'a> { Ok(blob.as_name().to_string()) } + /// Recode image to avatar size. pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> { - let img_wh = + let (img_wh, max_bytes) = match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) .unwrap_or_default() { - MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE, - MediaQuality::Worse => constants::WORSE_AVATAR_SIZE, + MediaQuality::Balanced => ( + constants::BALANCED_AVATAR_SIZE, + constants::BALANCED_AVATAR_BYTES, + ), + MediaQuality::Worse => { + (constants::WORSE_AVATAR_SIZE, constants::WORSE_AVATAR_BYTES) + } }; let maybe_sticker = &mut false; - let strict_limits = true; - // max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k. - // 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k. + let is_avatar = true; self.recode_to_size( context, None, // The name of an avatar doesn't matter maybe_sticker, img_wh, - 20_000, - strict_limits, + max_bytes, + is_avatar, )?; Ok(()) @@ -472,21 +304,17 @@ impl<'a> BlobObject<'a> { ), MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES), }; - let strict_limits = false; - let new_name = self.recode_to_size( - context, - name, - maybe_sticker, - img_wh, - max_bytes, - strict_limits, - )?; + let is_avatar = false; + let new_name = + self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?; Ok(new_name) } - /// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just - /// proceed with the result. + /// Recodes the image so that it fits into limits on width/height and byte size. + /// + /// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds + /// with the result without rechecking. /// /// This modifies the blob object in-place. /// @@ -501,10 +329,10 @@ impl<'a> BlobObject<'a> { maybe_sticker: &mut bool, mut img_wh: u32, max_bytes: usize, - strict_limits: bool, + is_avatar: bool, ) -> Result { // Add white background only to avatars to spare the CPU. - let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE; + let mut add_white_bg = is_avatar; let mut no_exif = false; let no_exif_ref = &mut no_exif; let mut name = name.unwrap_or_else(|| self.name.clone()); @@ -575,7 +403,7 @@ impl<'a> BlobObject<'a> { // also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated // images. let do_scale = exceeds_max_bytes - || strict_limits + || is_avatar && (exceeds_wh || exif.is_some() && { if mem::take(&mut add_white_bg) { @@ -604,7 +432,20 @@ impl<'a> BlobObject<'a> { if mem::take(&mut add_white_bg) { self::add_white_bg(&mut img); } - let new_img = img.thumbnail(img_wh, img_wh); + + // resize() results in often slightly better quality, + // however, comes at high price of being 4+ times slower than thumbnail(). + // for a typical camera image that is sent, this may be a change from "instant" (500ms) to "long time waiting" (3s). + // as we do not have recoding in background while chat has already a preview, + // we vote for speed. + // exception is the avatar image: this is far more often sent than recoded, + // usually has less pixels by cropping, UI that needs to wait anyways, + // and also benefits from slightly better (5%) encoding of Triangle-filtered images. + let new_img = if is_avatar { + img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle) + } else { + img.thumbnail(img_wh, img_wh) + }; if encoded_img_exceeds_bytes( context, @@ -612,7 +453,7 @@ impl<'a> BlobObject<'a> { ofmt.clone(), max_bytes, &mut encoded, - )? && strict_limits + )? && is_avatar { if img_wh < 20 { return Err(format_err!( @@ -662,7 +503,7 @@ impl<'a> BlobObject<'a> { match res { Ok(_) => res, Err(err) => { - if !strict_limits && no_exif { + if !is_avatar && no_exif { warn!( context, "Cannot recode image, using original data: {err:#}.", @@ -844,821 +685,4 @@ fn add_white_bg(img: &mut DynamicImage) { } #[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::message::{Message, Viewtype}; - use crate::sql; - use crate::test_utils::{self, TestContext}; - use crate::tools::SystemTime; - - fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage { - tokio::task::block_in_place(move || { - let img = ImageReader::open(path) - .expect("failed to open image") - .with_guessed_format() - .expect("failed to guess format") - .decode() - .expect("failed to decode image"); - assert_eq!(img.width(), width, "invalid width"); - assert_eq!(img.height(), height, "invalid height"); - img - }) - } - - const FILE_BYTES: &[u8] = b"hello"; - const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create() { - let t = TestContext::new().await; - let blob = - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); - let fname = t.get_blobdir().join(FILE_DEDUPLICATED); - let data = fs::read(fname).await.unwrap(); - assert_eq!(data, FILE_BYTES); - assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}")); - assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED)); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_lowercase_ext() { - let t = TestContext::new().await; - let blob = - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap(); - assert!( - blob.as_name().ends_with(".txt"), - "Blob {blob:?} should end with .txt" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_as_file_name() { - let t = TestContext::new().await; - let blob = - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); - assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_as_rel_path() { - let t = TestContext::new().await; - let blob = - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); - assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED)); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_suffix() { - let t = TestContext::new().await; - let blob = - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); - assert_eq!(blob.suffix(), Some("txt")); - let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap(); - assert_eq!(blob.suffix(), None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_dup() { - let t = TestContext::new().await; - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); - let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED); - assert!(foo_path.exists()); - BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap(); - let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap(); - while let Ok(Some(dirent)) = dir.next_entry().await { - let fname = dirent.file_name(); - if fname == foo_path.file_name().unwrap() { - assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES); - } else { - let name = fname.to_str().unwrap(); - assert!(name.ends_with(".txt")); - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_double_ext() { - let t = TestContext::new().await; - BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap(); - let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz"); - assert!(foo_path.exists()); - BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap(); - let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap(); - while let Ok(Some(dirent)) = dir.next_entry().await { - let fname = dirent.file_name(); - if fname == foo_path.file_name().unwrap() { - assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES); - } else { - let name = fname.to_str().unwrap(); - println!("{name}"); - assert_eq!(name.starts_with("foo"), false); - assert_eq!(name.ends_with(".tar.gz"), false); - assert!(name.ends_with(".gz")); - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_long_names() { - let t = TestContext::new().await; - let s = format!("file.{}", "a".repeat(100)); - let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap(); - let blobname = blob.as_name().split('/').last().unwrap(); - assert!(blobname.len() < 70); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_and_copy() { - let t = TestContext::new().await; - let src = t.dir.path().join("src"); - fs::write(&src, b"boo").await.unwrap(); - let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/src"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); - - let whoops = t.dir.path().join("whoops"); - assert!(BlobObject::create_and_copy(&t, whoops.as_ref()) - .await - .is_err()); - let whoops = t.get_blobdir().join("whoops"); - assert!(!whoops.exists()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_from_path() { - let t = TestContext::new().await; - - let src_ext = t.dir.path().join("external"); - fs::write(&src_ext, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, src_ext.as_ref()) - .await - .unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/external"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); - - let src_int = t.get_blobdir().join("internal"); - fs::write(&src_int, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/internal"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_from_name_long() { - let t = TestContext::new().await; - let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html"); - fs::write(&src_ext, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, src_ext.as_ref()) - .await - .unwrap(); - assert_eq!( - blob.as_name(), - "$BLOBDIR/autocrypt-setup-message-4137848473.html" - ); - } - - #[test] - fn test_is_blob_name() { - assert!(BlobObject::is_acceptible_blob_name("foo")); - assert!(BlobObject::is_acceptible_blob_name("foo.txt")); - assert!(BlobObject::is_acceptible_blob_name("f".repeat(128))); - assert!(!BlobObject::is_acceptible_blob_name("foo/bar")); - assert!(!BlobObject::is_acceptible_blob_name("foo\\bar")); - assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar")); - } - - #[test] - fn test_sanitise_name() { - let (stem, ext) = - BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt"); - assert_eq!(ext, ".txt"); - assert!(!stem.is_empty()); - - // the extensions are kept together as between stem and extension a number may be added - - // and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz` - let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz"); - assert_eq!(stem, "wot"); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitise_name(".foo.bar"); - assert_eq!(stem, ""); - assert_eq!(ext, ".foo.bar"); - - let (stem, ext) = BlobObject::sanitise_name("foo?.bar"); - assert!(stem.contains("foo")); - assert!(!stem.contains('?')); - assert_eq!(ext, ".bar"); - - let (stem, ext) = BlobObject::sanitise_name("no-extension"); - assert_eq!(stem, "no-extension"); - assert_eq!(ext, ""); - - let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c"); - assert_eq!(ext, ".c"); - assert!(!stem.contains("path")); - assert!(!stem.contains("ignored")); - assert!(stem.contains("this")); - assert!(stem.contains("forbidden")); - assert!(!stem.contains('/')); - assert!(!stem.contains('\\')); - assert!(!stem.contains(':')); - assert!(!stem.contains('*')); - assert!(!stem.contains('?')); - - let (stem, ext) = BlobObject::sanitise_name( - "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz", - ); - assert_eq!( - stem, - "file.with_lots_of_characters_behind_point_and_double_ending" - ); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz"); - assert_eq!(stem, "a. tar"); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf"); - assert_eq!(stem, "Guia_uso_GNB (v0.8)"); - assert_eq!(ext, ".pdf"); - } - - #[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, None, maybe_sticker, img_wh, 20_000, strict_limits) - .unwrap(); - tokio::task::block_in_place(move || { - let img = ImageReader::open(blob.to_abs_path()) - .unwrap() - .with_guessed_format() - .unwrap() - .decode() - .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() { - async fn file_size(path_buf: &Path) -> u64 { - fs::metadata(path_buf).await.unwrap().len() - } - - let t = TestContext::new().await; - let avatar_src = t.dir.path().join("avatar.jpg"); - let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - fs::write(&avatar_src, avatar_bytes).await.unwrap(); - t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await - .unwrap(); - let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); - let avatar_path = Path::new(&avatar_blob); - assert!( - avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"), - "The avatar filename should be its hash, put instead it's {avatar_blob}" - ); - let scaled_avatar_size = file_size(avatar_path).await; - assert!(scaled_avatar_size < avatar_bytes.len() as u64); - - check_image_size(avatar_src, 1000, 1000); - check_image_size( - &avatar_blob, - constants::BALANCED_AVATAR_SIZE, - constants::BALANCED_AVATAR_SIZE, - ); - - let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap(); - let maybe_sticker = &mut false; - let strict_limits = true; - blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits) - .unwrap(); - let new_file_size = file_size(&blob.to_abs_path()).await; - assert!(new_file_size <= 3000); - assert!(new_file_size > 2000); - // The new file should be smaller: - assert!(new_file_size < scaled_avatar_size); - // And the original file should not be touched: - assert_eq!(file_size(avatar_path).await, scaled_avatar_size); - tokio::task::block_in_place(move || { - let img = ImageReader::open(blob.to_abs_path()) - .unwrap() - .with_guessed_format() - .unwrap() - .decode() - .unwrap(); - assert!(img.width() > 130); - assert_eq!(img.width(), img.height()); - }); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_in_blobdir() { - let t = TestContext::new().await; - let avatar_src = t.get_blobdir().join("avatar.png"); - fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES) - .await - .unwrap(); - - check_image_size(&avatar_src, 900, 900); - - t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await - .unwrap(); - let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); - assert!( - avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"), - "Avatar file name {avatar_cfg} should end with its hash" - ); - - check_image_size( - avatar_cfg, - constants::BALANCED_AVATAR_SIZE, - constants::BALANCED_AVATAR_SIZE, - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_copy_without_recode() { - let t = TestContext::new().await; - let avatar_src = t.dir.path().join("avatar.png"); - let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); - fs::write(&avatar_src, avatar_bytes).await.unwrap(); - let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png"); - assert!(!avatar_blob.exists()); - t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await - .unwrap(); - assert!(avatar_blob.exists()); - assert_eq!( - fs::metadata(&avatar_blob).await.unwrap().len(), - avatar_bytes.len() as u64 - ); - let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap(); - assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_1() { - let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "0", - bytes, - extension: "jpg", - has_exif: true, - original_width: 1000, - original_height: 1000, - compressed_width: 1000, - compressed_height: 1000, - ..Default::default() - } - .test() - .await - .unwrap(); - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "1", - bytes, - extension: "jpg", - has_exif: true, - original_width: 1000, - original_height: 1000, - compressed_width: 1000, - compressed_height: 1000, - ..Default::default() - } - .test() - .await - .unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_2() { - // The "-rotated" files are rotated by 270 degrees using the Exif metadata - let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg"); - let img_rotated = SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "0", - bytes, - extension: "jpg", - has_exif: true, - original_width: 2000, - original_height: 1800, - orientation: 270, - compressed_width: 1800, - compressed_height: 2000, - ..Default::default() - } - .test() - .await - .unwrap(); - assert_correct_rotation(&img_rotated); - - let mut buf = Cursor::new(vec![]); - img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap(); - let bytes = buf.into_inner(); - - let img_rotated = SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "1", - bytes: &bytes, - extension: "jpg", - original_width: 1800, - original_height: 2000, - compressed_width: 1800, - compressed_height: 2000, - ..Default::default() - } - .test() - .await - .unwrap(); - assert_correct_rotation(&img_rotated); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_balanced_png() { - let bytes = include_bytes!("../test-data/image/screenshot.png"); - - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "0", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: 1920, - compressed_height: 1080, - ..Default::default() - } - .test() - .await - .unwrap(); - - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "1", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: constants::WORSE_IMAGE_SIZE, - compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, - ..Default::default() - } - .test() - .await - .unwrap(); - - SendImageCheckMediaquality { - viewtype: Viewtype::File, - media_quality_config: "1", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: 1920, - compressed_height: 1080, - ..Default::default() - } - .test() - .await - .unwrap(); - - SendImageCheckMediaquality { - viewtype: Viewtype::File, - media_quality_config: "1", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: 1920, - compressed_height: 1080, - set_draft: true, - ..Default::default() - } - .test() - .await - .unwrap(); - - // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation. - SendImageCheckMediaquality { - viewtype: Viewtype::Sticker, - media_quality_config: "0", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: 1920, - compressed_height: 1080, - ..Default::default() - } - .test() - .await - .unwrap(); - } - - /// Tests that RGBA PNG can be recoded into JPEG - /// by dropping alpha channel. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_rgba_png_to_jpeg() { - let bytes = include_bytes!("../test-data/image/screenshot-rgba.png"); - - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "1", - bytes, - extension: "png", - original_width: 1920, - original_height: 1080, - compressed_width: constants::WORSE_IMAGE_SIZE, - compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, - ..Default::default() - } - .test() - .await - .unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_recode_image_huge_jpg() { - let bytes = include_bytes!("../test-data/image/screenshot.jpg"); - SendImageCheckMediaquality { - viewtype: Viewtype::Image, - media_quality_config: "0", - bytes, - extension: "jpg", - has_exif: true, - original_width: 1920, - original_height: 1080, - compressed_width: constants::BALANCED_IMAGE_SIZE, - compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920, - ..Default::default() - } - .test() - .await - .unwrap(); - } - - fn assert_correct_rotation(img: &DynamicImage) { - // The test images are black in the bottom left corner after correctly applying - // the EXIF orientation - - let [luma] = img.get_pixel(10, 10).to_luma().0; - assert_eq!(luma, 255); - let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0; - assert_eq!(luma, 255); - let [luma] = img - .get_pixel(img.width() - 10, img.height() - 10) - .to_luma() - .0; - assert_eq!(luma, 255); - let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0; - assert_eq!(luma, 0); - } - - #[derive(Default)] - struct SendImageCheckMediaquality<'a> { - pub(crate) viewtype: Viewtype, - pub(crate) media_quality_config: &'a str, - pub(crate) bytes: &'a [u8], - pub(crate) extension: &'a str, - pub(crate) has_exif: bool, - pub(crate) original_width: u32, - pub(crate) original_height: u32, - pub(crate) orientation: i32, - pub(crate) compressed_width: u32, - pub(crate) compressed_height: u32, - pub(crate) set_draft: bool, - } - - impl SendImageCheckMediaquality<'_> { - pub(crate) async fn test(self) -> anyhow::Result { - let viewtype = self.viewtype; - let media_quality_config = self.media_quality_config; - let bytes = self.bytes; - let extension = self.extension; - let has_exif = self.has_exif; - let original_width = self.original_width; - let original_height = self.original_height; - let orientation = self.orientation; - let compressed_width = self.compressed_width; - let compressed_height = self.compressed_height; - let set_draft = self.set_draft; - - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice - .set_config(Config::MediaQuality, Some(media_quality_config)) - .await?; - let file = alice.get_blobdir().join("file").with_extension(extension); - let file_name = format!("file.{extension}"); - - fs::write(&file, &bytes) - .await - .context("failed to write file")?; - check_image_size(&file, original_width, original_height); - - let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?; - if has_exif { - let exif = exif.unwrap(); - assert_eq!(exif_orientation(&exif, &alice), orientation); - } else { - assert!(exif.is_none()); - } - - let mut msg = Message::new(viewtype); - msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?; - let chat = alice.create_chat(&bob).await; - if set_draft { - chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap(); - msg = chat.id.get_draft(&alice).await.unwrap().unwrap(); - assert_eq!(msg.get_viewtype(), Viewtype::File); - } - let sent = alice.send_msg(chat.id, &mut msg).await; - let alice_msg = alice.get_last_msg().await; - assert_eq!(alice_msg.get_width() as u32, compressed_width); - assert_eq!(alice_msg.get_height() as u32, compressed_height); - let file_saved = alice - .get_blobdir() - .join("saved-".to_string() + &alice_msg.get_filename().unwrap()); - alice_msg.save_file(&alice, &file_saved).await?; - check_image_size(file_saved, compressed_width, compressed_height); - - let bob_msg = bob.recv_msg(&sent).await; - assert_eq!(bob_msg.get_viewtype(), Viewtype::Image); - assert_eq!(bob_msg.get_width() as u32, compressed_width); - assert_eq!(bob_msg.get_height() as u32, compressed_height); - let file_saved = bob - .get_blobdir() - .join("saved-".to_string() + &bob_msg.get_filename().unwrap()); - bob_msg.save_file(&bob, &file_saved).await?; - if viewtype == Viewtype::File { - assert_eq!(file_saved.extension().unwrap(), extension); - let bytes1 = fs::read(&file_saved).await?; - assert_eq!(&bytes1, bytes); - } - - let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?; - assert!(exif.is_none()); - - let img = check_image_size(file_saved, compressed_width, compressed_height); - Ok(img) - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_big_gif_as_image() -> Result<()> { - let bytes = include_bytes!("../test-data/image/screenshot.gif"); - let (width, height) = (1920u32, 1080u32); - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice - .set_config( - Config::MediaQuality, - Some(&(MediaQuality::Worse as i32).to_string()), - ) - .await?; - let file = alice.get_blobdir().join("file").with_extension("gif"); - fs::write(&file, &bytes) - .await - .context("failed to write file")?; - let mut msg = Message::new(Viewtype::Image); - msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?; - let chat = alice.create_chat(&bob).await; - let sent = alice.send_msg(chat.id, &mut msg).await; - let bob_msg = bob.recv_msg(&sent).await; - // DC must detect the image as GIF and send it w/o reencoding. - assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif); - assert_eq!(bob_msg.get_width() as u32, width); - assert_eq!(bob_msg.get_height() as u32, height); - let file_saved = bob - .get_blobdir() - .join("saved-".to_string() + &bob_msg.get_filename().unwrap()); - bob_msg.save_file(&bob, &file_saved).await?; - let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?; - assert_eq!(file_size, bytes.len() as u64); - check_image_size(file_saved, width, height); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_gif_as_sticker() -> Result<()> { - let bytes = include_bytes!("../test-data/image/image100x50.gif"); - let alice = &TestContext::new_alice().await; - let file = alice.get_blobdir().join("file").with_extension("gif"); - fs::write(&file, &bytes) - .await - .context("failed to write file")?; - let mut msg = Message::new(Viewtype::Sticker); - msg.set_file_and_deduplicate(alice, &file, None, None)?; - let chat = alice.get_self_chat().await; - let sent = alice.send_msg(chat.id, &mut msg).await; - let msg = Message::load_from_db(alice, sent.sender_msg_id).await?; - // Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the - // extension. - assert_eq!(msg.get_viewtype(), Viewtype::Sticker); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_and_deduplicate() -> Result<()> { - let t = TestContext::new().await; - - let path = t.get_blobdir().join("anyfile.dat"); - fs::write(&path, b"bla").await?; - let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?; - assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat"); - assert_eq!(path.exists(), false); - - assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla"); - - fs::write(&path, b"bla").await?; - let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?; - assert_eq!(blob2.name, blob.name); - - let path_outside_blobdir = t.dir.path().join("anyfile.dat"); - fs::write(&path_outside_blobdir, b"bla").await?; - let blob3 = - BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?; - assert!(path_outside_blobdir.exists()); - assert_eq!(blob3.name, blob.name); - - fs::write(&path, b"blabla").await?; - let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?; - assert_ne!(blob4.name, blob.name); - - fs::remove_dir_all(t.get_blobdir()).await?; - let blob5 = - BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?; - assert_eq!(blob5.name, blob.name); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_and_deduplicate_from_bytes() -> Result<()> { - let t = TestContext::new().await; - - fs::remove_dir(t.get_blobdir()).await?; - let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?; - assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f"); - - assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla"); - let modified1 = blob.to_abs_path().metadata()?.modified()?; - - // Test that the modification time of the file is updated when a new file is created - // so that it's not deleted during housekeeping. - // We can't use SystemTime::shift() here because file creation uses the actual OS time, - // which we can't mock from our code. - tokio::time::sleep(Duration::from_millis(1100)).await; - - let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?; - assert_eq!(blob2.name, blob.name); - - let modified2 = blob.to_abs_path().metadata()?.modified()?; - assert_ne!(modified1, modified2); - sql::housekeeping(&t).await?; - assert!(blob2.to_abs_path().exists()); - - // If we do shift the time by more than 1h, the blob file will be deleted during housekeeping: - SystemTime::shift(Duration::from_secs(65 * 60)); - sql::housekeeping(&t).await?; - assert_eq!(blob2.to_abs_path().exists(), false); - - let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?; - assert_ne!(blob3.name, blob.name); - - { - // If something goes wrong and the blob file is overwritten, - // the correct content should be restored: - fs::write(blob3.to_abs_path(), b"bloblo").await?; - - let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?; - let blob4_content = fs::read(blob4.to_abs_path()).await?; - assert_eq!(blob4_content, b"blabla"); - } - - Ok(()) - } -} +mod blob_tests; diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs new file mode 100644 index 0000000000..47132baf26 --- /dev/null +++ b/src/blob/blob_tests.rs @@ -0,0 +1,750 @@ +use std::time::Duration; + +use super::*; +use crate::message::{Message, Viewtype}; +use crate::param::Param; +use crate::sql; +use crate::test_utils::{self, TestContext}; +use crate::tools::SystemTime; + +fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage { + tokio::task::block_in_place(move || { + let img = ImageReader::open(path) + .expect("failed to open image") + .with_guessed_format() + .expect("failed to guess format") + .decode() + .expect("failed to decode image"); + assert_eq!(img.width(), width, "invalid width"); + assert_eq!(img.height(), height, "invalid height"); + img + }) +} + +const FILE_BYTES: &[u8] = b"hello"; +const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create() { + let t = TestContext::new().await; + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); + let fname = t.get_blobdir().join(FILE_DEDUPLICATED); + let data = fs::read(fname).await.unwrap(); + assert_eq!(data, FILE_BYTES); + assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}")); + assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_lowercase_ext() { + let t = TestContext::new().await; + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap(); + assert!( + blob.as_name().ends_with(".txt"), + "Blob {blob:?} should end with .txt" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_suffix() { + let t = TestContext::new().await; + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); + assert_eq!(blob.suffix(), Some("txt")); + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap(); + assert_eq!(blob.suffix(), None); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_dup() { + let t = TestContext::new().await; + BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap(); + let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED); + assert!(foo_path.exists()); + BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap(); + let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap(); + while let Ok(Some(dirent)) = dir.next_entry().await { + let fname = dirent.file_name(); + if fname == foo_path.file_name().unwrap() { + assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES); + } else { + let name = fname.to_str().unwrap(); + assert!(name.ends_with(".txt")); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_double_ext() { + let t = TestContext::new().await; + BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap(); + let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz"); + assert!(foo_path.exists()); + BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap(); + let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap(); + while let Ok(Some(dirent)) = dir.next_entry().await { + let fname = dirent.file_name(); + if fname == foo_path.file_name().unwrap() { + assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES); + } else { + let name = fname.to_str().unwrap(); + println!("{name}"); + assert_eq!(name.starts_with("foo"), false); + assert_eq!(name.ends_with(".tar.gz"), false); + assert!(name.ends_with(".gz")); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_long_names() { + let t = TestContext::new().await; + let s = format!("file.{}", "a".repeat(100)); + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap(); + let blobname = blob.as_name().split('/').next_back().unwrap(); + assert!(blobname.len() < 70); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_from_name_long() { + let t = TestContext::new().await; + let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html"); + fs::write(&src_ext, b"boo").await.unwrap(); + let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap(); + assert_eq!( + blob.as_name(), + "$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html" + ); +} + +#[test] +fn test_is_blob_name() { + assert!(BlobObject::is_acceptible_blob_name("foo")); + assert!(BlobObject::is_acceptible_blob_name("foo.txt")); + assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128))); + assert!(!BlobObject::is_acceptible_blob_name("foo/bar")); + assert!(!BlobObject::is_acceptible_blob_name("foo\\bar")); + assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar")); +} + +#[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::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap(); + let img_wh = 128; + let maybe_sticker = &mut false; + let strict_limits = true; + blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits) + .unwrap(); + tokio::task::block_in_place(move || { + let img = ImageReader::open(blob.to_abs_path()) + .unwrap() + .with_guessed_format() + .unwrap() + .decode() + .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() { + async fn file_size(path_buf: &Path) -> u64 { + fs::metadata(path_buf).await.unwrap().len() + } + + let t = TestContext::new().await; + let avatar_src = t.dir.path().join("avatar.jpg"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg"); + fs::write(&avatar_src, avatar_bytes).await.unwrap(); + t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await + .unwrap(); + let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + let avatar_path = Path::new(&avatar_blob); + assert!( + avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"), + "The avatar filename should be its hash, put instead it's {avatar_blob}" + ); + let scaled_avatar_size = file_size(avatar_path).await; + assert!(scaled_avatar_size < avatar_bytes.len() as u64); + + check_image_size(avatar_src, 1000, 1000); + check_image_size( + &avatar_blob, + constants::BALANCED_AVATAR_SIZE, + constants::BALANCED_AVATAR_SIZE, + ); + + let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap(); + let maybe_sticker = &mut false; + let strict_limits = true; + blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits) + .unwrap(); + let new_file_size = file_size(&blob.to_abs_path()).await; + assert!(new_file_size <= 3000); + assert!(new_file_size > 2000); + // The new file should be smaller: + assert!(new_file_size < scaled_avatar_size); + // And the original file should not be touched: + assert_eq!(file_size(avatar_path).await, scaled_avatar_size); + tokio::task::block_in_place(move || { + let img = ImageReader::open(blob.to_abs_path()) + .unwrap() + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); + assert!(img.width() > 130); + assert_eq!(img.width(), img.height()); + }); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_in_blobdir() { + let t = TestContext::new().await; + let avatar_src = t.get_blobdir().join("avatar.png"); + fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES) + .await + .unwrap(); + + check_image_size(&avatar_src, 900, 900); + + t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await + .unwrap(); + let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + assert!( + avatar_cfg.ends_with("d57cb5ce5f371531b6e1fb17b6dd1af.png"), + "Avatar file name {avatar_cfg} should end with its hash" + ); + + check_image_size( + avatar_cfg, + constants::BALANCED_AVATAR_SIZE, + constants::BALANCED_AVATAR_SIZE, + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_copy_without_recode() { + let t = TestContext::new().await; + let avatar_src = t.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + fs::write(&avatar_src, avatar_bytes).await.unwrap(); + let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png"); + assert!(!avatar_blob.exists()); + t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await + .unwrap(); + assert!(avatar_blob.exists()); + assert_eq!( + fs::metadata(&avatar_blob).await.unwrap().len(), + avatar_bytes.len() as u64 + ); + let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap(); + assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_1() { + let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg"); + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "jpg", + has_exif: true, + original_width: 1000, + original_height: 1000, + compressed_width: 1000, + compressed_height: 1000, + ..Default::default() + } + .test() + .await + .unwrap(); + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "1", + bytes, + extension: "jpg", + has_exif: true, + original_width: 1000, + original_height: 1000, + compressed_width: 1000, + compressed_height: 1000, + ..Default::default() + } + .test() + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_2() { + // The "-rotated" files are rotated by 270 degrees using the Exif metadata + let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg"); + let img_rotated = SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "jpg", + has_exif: true, + original_width: 2000, + original_height: 1800, + orientation: 270, + compressed_width: 1800, + compressed_height: 2000, + ..Default::default() + } + .test() + .await + .unwrap(); + assert_correct_rotation(&img_rotated); + + let mut buf = Cursor::new(vec![]); + img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap(); + let bytes = buf.into_inner(); + + let img_rotated = SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "1", + bytes: &bytes, + extension: "jpg", + original_width: 1800, + original_height: 2000, + compressed_width: 1800, + compressed_height: 2000, + ..Default::default() + } + .test() + .await + .unwrap(); + assert_correct_rotation(&img_rotated); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_balanced_png() { + let bytes = include_bytes!("../../test-data/image/screenshot.png"); + + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: 1920, + compressed_height: 1080, + ..Default::default() + } + .test() + .await + .unwrap(); + + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "1", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: constants::WORSE_IMAGE_SIZE, + compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, + ..Default::default() + } + .test() + .await + .unwrap(); + + SendImageCheckMediaquality { + viewtype: Viewtype::File, + media_quality_config: "1", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: 1920, + compressed_height: 1080, + ..Default::default() + } + .test() + .await + .unwrap(); + + SendImageCheckMediaquality { + viewtype: Viewtype::File, + media_quality_config: "1", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: 1920, + compressed_height: 1080, + set_draft: true, + ..Default::default() + } + .test() + .await + .unwrap(); + + // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation. + SendImageCheckMediaquality { + viewtype: Viewtype::Sticker, + media_quality_config: "0", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: 1920, + compressed_height: 1080, + ..Default::default() + } + .test() + .await + .unwrap(); +} + +/// Tests that RGBA PNG can be recoded into JPEG +/// by dropping alpha channel. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_rgba_png_to_jpeg() { + let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png"); + + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "1", + bytes, + extension: "png", + original_width: 1920, + original_height: 1080, + compressed_width: constants::WORSE_IMAGE_SIZE, + compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920, + ..Default::default() + } + .test() + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_recode_image_huge_jpg() { + let bytes = include_bytes!("../../test-data/image/screenshot.jpg"); + SendImageCheckMediaquality { + viewtype: Viewtype::Image, + media_quality_config: "0", + bytes, + extension: "jpg", + has_exif: true, + original_width: 1920, + original_height: 1080, + compressed_width: constants::BALANCED_IMAGE_SIZE, + compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920, + ..Default::default() + } + .test() + .await + .unwrap(); +} + +fn assert_correct_rotation(img: &DynamicImage) { + // The test images are black in the bottom left corner after correctly applying + // the EXIF orientation + + let [luma] = img.get_pixel(10, 10).to_luma().0; + assert_eq!(luma, 255); + let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0; + assert_eq!(luma, 255); + let [luma] = img + .get_pixel(img.width() - 10, img.height() - 10) + .to_luma() + .0; + assert_eq!(luma, 255); + let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0; + assert_eq!(luma, 0); +} + +#[derive(Default)] +struct SendImageCheckMediaquality<'a> { + pub(crate) viewtype: Viewtype, + pub(crate) media_quality_config: &'a str, + pub(crate) bytes: &'a [u8], + pub(crate) extension: &'a str, + pub(crate) has_exif: bool, + pub(crate) original_width: u32, + pub(crate) original_height: u32, + pub(crate) orientation: i32, + pub(crate) compressed_width: u32, + pub(crate) compressed_height: u32, + pub(crate) set_draft: bool, +} + +impl SendImageCheckMediaquality<'_> { + pub(crate) async fn test(self) -> anyhow::Result { + let viewtype = self.viewtype; + let media_quality_config = self.media_quality_config; + let bytes = self.bytes; + let extension = self.extension; + let has_exif = self.has_exif; + let original_width = self.original_width; + let original_height = self.original_height; + let orientation = self.orientation; + let compressed_width = self.compressed_width; + let compressed_height = self.compressed_height; + let set_draft = self.set_draft; + + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice + .set_config(Config::MediaQuality, Some(media_quality_config)) + .await?; + let file = alice.get_blobdir().join("file").with_extension(extension); + let file_name = format!("file.{extension}"); + + fs::write(&file, &bytes) + .await + .context("failed to write file")?; + check_image_size(&file, original_width, original_height); + + let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?; + if has_exif { + let exif = exif.unwrap(); + assert_eq!(exif_orientation(&exif, &alice), orientation); + } else { + assert!(exif.is_none()); + } + + let mut msg = Message::new(viewtype); + msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?; + let chat = alice.create_chat(&bob).await; + if set_draft { + chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap(); + msg = chat.id.get_draft(&alice).await.unwrap().unwrap(); + assert_eq!(msg.get_viewtype(), Viewtype::File); + } + let sent = alice.send_msg(chat.id, &mut msg).await; + let alice_msg = alice.get_last_msg().await; + assert_eq!(alice_msg.get_width() as u32, compressed_width); + assert_eq!(alice_msg.get_height() as u32, compressed_height); + let file_saved = alice + .get_blobdir() + .join("saved-".to_string() + &alice_msg.get_filename().unwrap()); + alice_msg.save_file(&alice, &file_saved).await?; + check_image_size(file_saved, compressed_width, compressed_height); + + if original_width == compressed_width { + assert_extension(&alice, alice_msg, extension); + } else { + assert_extension(&alice, alice_msg, "jpg"); + } + + let bob_msg = bob.recv_msg(&sent).await; + assert_eq!(bob_msg.get_viewtype(), Viewtype::Image); + assert_eq!(bob_msg.get_width() as u32, compressed_width); + assert_eq!(bob_msg.get_height() as u32, compressed_height); + let file_saved = bob + .get_blobdir() + .join("saved-".to_string() + &bob_msg.get_filename().unwrap()); + bob_msg.save_file(&bob, &file_saved).await?; + if viewtype == Viewtype::File { + assert_eq!(file_saved.extension().unwrap(), extension); + let bytes1 = fs::read(&file_saved).await?; + assert_eq!(&bytes1, bytes); + } + + let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?; + assert!(exif.is_none()); + + let img = check_image_size(file_saved, compressed_width, compressed_height); + + if original_width == compressed_width { + assert_extension(&bob, bob_msg, extension); + } else { + assert_extension(&bob, bob_msg, "jpg"); + } + + Ok(img) + } +} + +fn assert_extension(context: &TestContext, msg: Message, extension: &str) { + assert!(msg + .param + .get(Param::File) + .unwrap() + .ends_with(&format!(".{extension}"))); + assert!(msg + .param + .get(Param::Filename) + .unwrap() + .ends_with(&format!(".{extension}"))); + assert!(msg + .get_filename() + .unwrap() + .ends_with(&format!(".{extension}"))); + assert_eq!( + msg.get_file(context) + .unwrap() + .extension() + .unwrap() + .to_str() + .unwrap(), + extension + ); + assert_eq!( + msg.param + .get_file_blob(context) + .unwrap() + .unwrap() + .suffix() + .unwrap(), + extension + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_big_gif_as_image() -> Result<()> { + let bytes = include_bytes!("../../test-data/image/screenshot.gif"); + let (width, height) = (1920u32, 1080u32); + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice + .set_config( + Config::MediaQuality, + Some(&(MediaQuality::Worse as i32).to_string()), + ) + .await?; + let file = alice.get_blobdir().join("file").with_extension("gif"); + fs::write(&file, &bytes) + .await + .context("failed to write file")?; + let mut msg = Message::new(Viewtype::Image); + msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?; + let chat = alice.create_chat(&bob).await; + let sent = alice.send_msg(chat.id, &mut msg).await; + let bob_msg = bob.recv_msg(&sent).await; + // DC must detect the image as GIF and send it w/o reencoding. + assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif); + assert_eq!(bob_msg.get_width() as u32, width); + assert_eq!(bob_msg.get_height() as u32, height); + let file_saved = bob + .get_blobdir() + .join("saved-".to_string() + &bob_msg.get_filename().unwrap()); + bob_msg.save_file(&bob, &file_saved).await?; + let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?; + assert_eq!(file_size, bytes.len() as u64); + check_image_size(file_saved, width, height); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_gif_as_sticker() -> Result<()> { + let bytes = include_bytes!("../../test-data/image/image100x50.gif"); + let alice = &TestContext::new_alice().await; + let file = alice.get_blobdir().join("file").with_extension("gif"); + fs::write(&file, &bytes) + .await + .context("failed to write file")?; + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file_and_deduplicate(alice, &file, None, None)?; + let chat = alice.get_self_chat().await; + let sent = alice.send_msg(chat.id, &mut msg).await; + let msg = Message::load_from_db(alice, sent.sender_msg_id).await?; + // Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the + // extension. + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_and_deduplicate() -> Result<()> { + let t = TestContext::new().await; + + let path = t.get_blobdir().join("anyfile.dat"); + fs::write(&path, b"bla").await?; + let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?; + assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat"); + assert_eq!(path.exists(), false); + + assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla"); + + fs::write(&path, b"bla").await?; + let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?; + assert_eq!(blob2.name, blob.name); + + let path_outside_blobdir = t.dir.path().join("anyfile.dat"); + fs::write(&path_outside_blobdir, b"bla").await?; + let blob3 = + BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?; + assert!(path_outside_blobdir.exists()); + assert_eq!(blob3.name, blob.name); + + fs::write(&path, b"blabla").await?; + let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?; + assert_ne!(blob4.name, blob.name); + + fs::remove_dir_all(t.get_blobdir()).await?; + let blob5 = + BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?; + assert_eq!(blob5.name, blob.name); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_and_deduplicate_from_bytes() -> Result<()> { + let t = TestContext::new().await; + + fs::remove_dir(t.get_blobdir()).await?; + let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?; + assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f"); + + assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla"); + let modified1 = blob.to_abs_path().metadata()?.modified()?; + + // Test that the modification time of the file is updated when a new file is created + // so that it's not deleted during housekeeping. + // We can't use SystemTime::shift() here because file creation uses the actual OS time, + // which we can't mock from our code. + tokio::time::sleep(Duration::from_millis(1100)).await; + + let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?; + assert_eq!(blob2.name, blob.name); + + let modified2 = blob.to_abs_path().metadata()?.modified()?; + assert_ne!(modified1, modified2); + sql::housekeeping(&t).await?; + assert!(blob2.to_abs_path().exists()); + + // If we do shift the time by more than 1h, the blob file will be deleted during housekeeping: + SystemTime::shift(Duration::from_secs(65 * 60)); + sql::housekeeping(&t).await?; + assert_eq!(blob2.to_abs_path().exists(), false); + + let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?; + assert_ne!(blob3.name, blob.name); + + { + // If something goes wrong and the blob file is overwritten, + // the correct content should be restored: + fs::write(blob3.to_abs_path(), b"bloblo").await?; + + let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?; + let blob4_content = fs::read(blob4.to_abs_path()).await?; + assert_eq!(blob4_content, b"blabla"); + } + + Ok(()) +} diff --git a/src/chat.rs b/src/chat.rs index be256e5984..0c5fa97922 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3,6 +3,7 @@ use std::cmp; use std::collections::{HashMap, HashSet}; use std::fmt; +use std::io::Cursor; use std::marker::Sync; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -11,6 +12,7 @@ use std::time::Duration; use anyhow::{anyhow, bail, ensure, Context as _, Result}; use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress}; use deltachat_derive::{FromSql, ToSql}; +use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use tokio::task; @@ -18,12 +20,11 @@ use tokio::task; use crate::aheader::EncryptPreference; use crate::blob::BlobObject; use crate::chatlist::Chatlist; -use crate::chatlist_events; use crate::color::str_to_color; use crate::config::Config; use crate::constants::{ self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, - DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, + DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE, }; use crate::contact::{self, Contact, ContactId, Origin}; @@ -32,7 +33,6 @@ use crate::debug_logging::maybe_set_logging_xdc; use crate::download::DownloadState; use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer}; use crate::events::EventType; -use crate::html::new_html_mimepart; use crate::location; use crate::log::LogExt; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; @@ -41,7 +41,6 @@ use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::receive_imf::ReceivedMsg; -use crate::securejoin::BobState; use crate::smtp::send_msg_to_smtp; use crate::stock_str; use crate::sync::{self, Sync::*, SyncData}; @@ -51,6 +50,7 @@ use crate::tools::{ truncate_msg_text, IsNoneOrEmpty, SystemTime, }; use crate::webxdc::StatusUpdateSerial; +use crate::{chatlist_events, imap}; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -130,8 +130,7 @@ pub(crate) enum CantSendReason { /// Not a member of the chat. NotAMember, - /// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending - /// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed. + /// Temporary state for 1:1 chats while SecureJoin is in progress. SecurejoinWait, } @@ -434,7 +433,7 @@ impl ChatId { .ok(); } if delete { - self.delete(context).await?; + self.delete_ex(context, Nosync).await?; } Ok(()) } @@ -582,7 +581,18 @@ impl ChatId { ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled, }; - add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?; + add_info_msg_with_cmd( + context, + self, + &text, + cmd, + timestamp_sort, + None, + None, + None, + None, + ) + .await?; Ok(()) } @@ -643,7 +653,7 @@ impl ChatId { ) -> Result<()> { let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes) .await - .with_context(|| format!("can't create chat for {}", contact_id))?; + .with_context(|| format!("can't create chat for {contact_id}"))?; chat_id .set_protection( context, @@ -773,6 +783,10 @@ impl ChatId { /// Deletes a chat. pub async fn delete(self, context: &Context) -> Result<()> { + self.delete_ex(context, Sync).await + } + + pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> { ensure!( !self.is_special(), "bad chat_id, can not be a special chat: {}", @@ -780,10 +794,23 @@ impl ChatId { ); let chat = Chat::load_from_db(context, self).await?; + let delete_msgs_target = context.get_delete_msgs_target().await?; + let sync_id = match sync { + Nosync => None, + Sync => chat.get_sync_id(context).await?, + }; context .sql .transaction(|transaction| { + transaction.execute( + "UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)", + (delete_msgs_target, self,), + )?; + transaction.execute( + "DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)", + (self,), + )?; transaction.execute( "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)", (self,), @@ -795,13 +822,15 @@ impl ChatId { }) .await?; + context.emit_event(EventType::ChatDeleted { chat_id: self }); context.emit_msgs_changed_without_ids(); - chatlist_events::emit_chatlist_changed(context); - context - .set_config_internal(Config::LastHousekeeping, None) - .await?; - context.scheduler.interrupt_inbox().await; + if let Some(id) = sync_id { + self::sync(context, id, SyncAction::Delete) + .await + .log_err(context) + .ok(); + } if chat.is_self_talk() { let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await); @@ -809,6 +838,11 @@ impl ChatId { } chatlist_events::emit_chatlist_changed(context); + context + .set_config_internal(Config::LastHousekeeping, None) + .await?; + context.scheduler.interrupt_inbox().await; + Ok(()) } @@ -890,12 +924,6 @@ impl ChatId { } } _ => { - let blob = msg - .param - .get_blob(Param::File, context) - .await? - .context("no file stored in params")?; - msg.param.set(Param::File, blob.as_name()); if msg.viewtype == Viewtype::File { if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) // We do not do an automatic conversion to other viewtypes here so that @@ -908,6 +936,10 @@ impl ChatId { } } if msg.viewtype == Viewtype::Vcard { + let blob = msg + .param + .get_file_blob(context)? + .context("no file stored in params")?; msg.try_set_vcard(context, &blob.to_abs_path()).await?; } } @@ -963,6 +995,7 @@ impl ChatId { transaction.execute( "INSERT INTO msgs ( chat_id, + rfc724_mid, from_id, timestamp, type, @@ -972,9 +1005,10 @@ impl ChatId { param, hidden, mime_in_reply_to) - VALUES (?,?,?,?,?,?,?,?,?,?);", + VALUES (?,?,?,?,?,?,?,?,?,?,?);", ( self, + &msg.rfc724_mid, ContactId::SELF, time(), msg.viewtype, @@ -1050,7 +1084,16 @@ impl ChatId { Ok(count) } - /// Returns timestamp of the latest message in the chat. + pub(crate) async fn created_timestamp(self, context: &Context) -> Result { + Ok(context + .sql + .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,)) + .await? + .unwrap_or(0)) + } + + /// Returns timestamp of the latest message in the chat, + /// including hidden messages or a draft if there is one. pub(crate) async fn get_timestamp(self, context: &Context) -> Result> { let timestamp = context .sql @@ -1283,8 +1326,7 @@ impl ChatId { /// /// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`]. pub async fn get_encryption_info(self, context: &Context) -> Result { - let mut ret_mutual = String::new(); - let mut ret_nopreference = String::new(); + let mut ret_available = String::new(); let mut ret_reset = String::new(); for contact_id in get_chat_contacts(context, self) @@ -1300,8 +1342,9 @@ impl ChatId { .filter(|peerstate| peerstate.peek_key(false).is_some()) .map(|peerstate| peerstate.prefer_encrypt) { - Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"), - Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"), + Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => { + ret_available += &format!("{addr}\n") + } Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"), }; } @@ -1313,23 +1356,14 @@ impl ChatId { ret.push('\n'); ret += &ret_reset; } - if !ret_nopreference.is_empty() { + if !ret_available.is_empty() { if !ret.is_empty() { ret.push('\n'); } ret += &stock_str::e2e_available(context).await; ret.push(':'); ret.push('\n'); - ret += &ret_nopreference; - } - if !ret_mutual.is_empty() { - if !ret.is_empty() { - ret.push('\n'); - } - ret += &stock_str::e2e_preferred(context).await; - ret.push(':'); - ret.push('\n'); - ret += &ret_mutual; + ret += &ret_available; } Ok(ret.trim().to_string()) @@ -1344,41 +1378,10 @@ impl ChatId { } pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> { - self.set_gossiped_timestamp(context, 0).await - } - - /// Get timestamp of the last gossip sent in the chat. - /// Zero return value means that gossip was never sent. - pub async fn get_gossiped_timestamp(self, context: &Context) -> Result { - let timestamp: Option = context - .sql - .query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,)) - .await?; - Ok(timestamp.unwrap_or_default()) - } - - pub(crate) async fn set_gossiped_timestamp( - self, - context: &Context, - timestamp: i64, - ) -> Result<()> { - ensure!( - !self.is_special(), - "can not set gossiped timestamp for special chats" - ); - info!( - context, - "Set gossiped_timestamp for chat {} to {}.", self, timestamp, - ); - context .sql - .execute( - "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", - (timestamp, self), - ) + .execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,)) .await?; - Ok(()) } @@ -1714,8 +1717,8 @@ impl Chat { /// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin. /// - /// If the timeout has expired, notifies the user that sending messages is possible. See also - /// [`CantSendReason::SecurejoinWait`]. + /// If the timeout has expired, adds an info message with additional information. + /// See also [`CantSendReason::SecurejoinWait`]. pub(crate) async fn check_securejoin_wait( &self, context: &Context, @@ -1724,16 +1727,19 @@ impl Chat { if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected { return Ok(0); } - let (mut param0, mut param1) = (Params::new(), Params::new()); - param0.set_cmd(SystemMessage::SecurejoinWait); - param1.set_cmd(SystemMessage::SecurejoinWaitTimeout); - let (param0, param1) = (param0.to_string(), param1.to_string()); + + // chat is single and unprotected: + // get last info message of type SecurejoinWait or SecurejoinWaitTimeout + let (mut param_wait, mut param_timeout) = (Params::new(), Params::new()); + param_wait.set_cmd(SystemMessage::SecurejoinWait); + param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout); + let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string()); let Some((param, ts_sort, ts_start)) = context .sql .query_row_optional( "SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\ (SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))", - (self.id, ¶m0, ¶m1), + (self.id, ¶m_wait, ¶m_timeout), |row| { let param: String = row.get(0)?; let ts_sort: i64 = row.get(1)?; @@ -1745,9 +1751,10 @@ impl Chat { else { return Ok(0); }; - if param == param1 { + if param == param_timeout { return Ok(0); } + let now = time(); // Don't await SecureJoin if the clock was set back. if ts_start <= now { @@ -1761,7 +1768,7 @@ impl Chat { add_info_msg_with_cmd( context, self.id, - &stock_str::securejoin_wait_timeout(context).await, + &stock_str::securejoin_takes_longer(context).await, SystemMessage::SecurejoinWaitTimeout, // Use the sort timestamp of the "please wait" message, this way the added message is // never sorted below the protection message if the SecureJoin finishes in parallel. @@ -1769,6 +1776,7 @@ impl Chat { Some(now), None, None, + None, ) .await?; context.emit_event(EventType::ChatModified(self.id)); @@ -1877,7 +1885,6 @@ impl Chat { name: self.name.clone(), archived: self.visibility == ChatVisibility::Archived, param: self.param.to_string(), - gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?, is_sending_locations: self.is_sending_locations, color: self.get_color(context).await?, profile_image: self @@ -1968,13 +1975,7 @@ impl Chat { if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) { Ok(member_list_timestamp) } else { - let creation_timestamp: i64 = context - .sql - .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self.id,)) - .await - .context("SQL error querying created_timestamp")? - .context("Chat not found")?; - Ok(creation_timestamp) + Ok(self.id.created_timestamp(context).await?) } } @@ -2006,7 +2007,9 @@ impl Chat { let mut to_id = 0; let mut location_id = 0; - let new_rfc724_mid = create_outgoing_rfc724_mid(); + if msg.rfc724_mid.is_empty() { + msg.rfc724_mid = create_outgoing_rfc724_mid(); + } if self.typ == Chattype::Single { if let Some(id) = context @@ -2029,7 +2032,9 @@ impl Chat { && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { msg.param.set_int(Param::AttachGroupImage, 1); - self.param.remove(Param::Unpromoted); + self.param + .remove(Param::Unpromoted) + .set_i64(Param::GroupNameTimestamp, timestamp); self.update_param(context).await?; // TODO: Remove this compat code needed because Core <= v1.143: // - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also @@ -2102,7 +2107,7 @@ impl Chat { 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(); + new_references = msg.rfc724_mid.clone(); } else { new_references = references_vec.join(" "); } @@ -2112,7 +2117,7 @@ impl Chat { // 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(); + new_references = msg.rfc724_mid.clone(); } // add independent location to database @@ -2157,14 +2162,18 @@ impl Chat { } else { None }; - let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string()); + let new_mime_headers: Option = new_mime_headers.map(|s| { + let html_part = MimePart::new("text/html", s); + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + html_part.write_part(cursor).ok(); + String::from_utf8_lossy(&buffer).to_string() + }); let new_mime_headers = new_mime_headers.or_else(|| match was_truncated { // We need to add some headers so that they are stripped before formatting HTML by // `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's // anyway a useful metadata about the stored text. - true => Some( - "Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n", - ), + true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text), false => None, }); let new_mime_headers = match new_mime_headers { @@ -2176,7 +2185,6 @@ impl Chat { msg.chat_id = self.id; msg.from_id = ContactId::SELF; - msg.rfc724_mid = new_rfc724_mid; msg.timestamp_sort = timestamp; // add message to the database @@ -2298,6 +2306,10 @@ impl Chat { async fn get_sync_id(&self, context: &Context) -> Result> { match self.typ { Chattype::Single => { + if self.is_device_talk() { + return Ok(Some(SyncId::Device)); + } + let mut r = None; for contact_id in get_chat_contacts(context, self.id).await? { if contact_id == ContactId::SELF && !self.is_self_talk() { @@ -2416,9 +2428,6 @@ pub struct ChatInfo { /// This is the string-serialised version of `Params` currently. pub param: String, - /// Last time this client sent autocrypt gossip headers to this chat. - pub gossiped_timestamp: i64, - /// Whether this chat is currently sending location-stream messages. pub is_sending_locations: bool, @@ -2560,19 +2569,27 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> { /// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task /// unblocking the chat and notifying the user accordingly. pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> { - let Some(bobstate) = BobState::from_db(&context.sql).await? else { - return Ok(()); - }; - if !bobstate.in_progress() { - return Ok(()); - } - let chat_id = bobstate.alice_chat(); - let chat = Chat::load_from_db(context, chat_id).await?; - let timeout = chat - .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT) + let chat_ids: Vec = context + .sql + .query_map( + "SELECT chat_id FROM bobstate", + (), + |row| { + let chat_id: ChatId = row.get(0)?; + Ok(chat_id) + }, + |rows| rows.collect::, _>>().map_err(Into::into), + ) .await?; - if timeout > 0 { - chat_id.spawn_securejoin_wait(context, timeout); + + for chat_id in chat_ids { + let chat = Chat::load_from_db(context, chat_id).await?; + let timeout = chat + .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT) + .await?; + if timeout > 0 { + chat_id.spawn_securejoin_wait(context, timeout); + } } Ok(()) } @@ -2736,8 +2753,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { } else if msg.viewtype.has_file() { let mut blob = msg .param - .get_blob(Param::File, context) - .await? + .get_file_blob(context)? .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?; let send_as_is = msg.viewtype == Viewtype::File; @@ -2785,20 +2801,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { .recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker) .await?; msg.param.set(Param::Filename, new_name); + msg.param.set(Param::File, blob.as_name()); if !maybe_sticker { msg.viewtype = Viewtype::Image; } } - msg.param.set(Param::File, blob.as_name()); - if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) { - let stem = match filename.rsplit_once('.') { - Some((stem, _)) => stem, - None => filename, - }; - msg.param - .set(Param::Filename, stem.to_string() + "." + blob_ext); - } if !msg.param.exists(Param::MimeType) { if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) { @@ -2981,6 +2989,12 @@ async fn prepare_send_msg( /// /// The caller has to interrupt SMTP loop or otherwise process new rows. pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result> { + if msg.param.get_cmd() == SystemMessage::GroupNameChanged { + msg.chat_id + .update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort) + .await?; + } + let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default(); let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?; let attach_selfavatar = mimefactory.attach_selfavatar; @@ -3001,10 +3015,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - // disabled by default is fine. // // `from` must be the last addr, see `receive_imf_inner()` why. - if context.get_config_bool(Config::BccSelf).await? - && !recipients - .iter() - .any(|x| x.to_lowercase() == lowercase_from) + recipients.retain(|x| x.to_lowercase() != lowercase_from); + if (context.get_config_bool(Config::BccSelf).await? + || msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage) + && (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty()) { recipients.push(from); } @@ -3020,6 +3034,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - context, "Message {} has no recipient, skipping smtp-send.", msg.id ); + msg.param.set_int(Param::GuaranteeE2ee, 1); + msg.update_param(context).await?; msg.id.set_delivered(context).await?; msg.state = MessageState::OutDelivered; return Ok(Vec::new()); @@ -3050,10 +3066,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - let now = smeared_time(context); - if rendered_msg.is_gossiped { - msg.chat_id.set_gossiped_timestamp(context, now).await?; - } - if rendered_msg.last_added_location_id.is_some() { if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await { error!(context, "Failed to set kml sent_timestamp: {err:#}."); @@ -3124,6 +3136,66 @@ pub async fn send_text_msg( send_msg(context, chat_id, &mut msg).await } +/// Sends chat members a request to edit the given message's text. +pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> { + let mut original_msg = Message::load_from_db(context, msg_id).await?; + ensure!( + original_msg.from_id == ContactId::SELF, + "Can edit only own messages" + ); + ensure!(!original_msg.is_info(), "Cannot edit info messages"); + ensure!(!original_msg.has_html(), "Cannot edit HTML messages"); + ensure!( + original_msg.viewtype != Viewtype::VideochatInvitation, + "Cannot edit videochat invitations" + ); + ensure!( + !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings + "Cannot add text" + ); + ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty"); + if original_msg.text == new_text { + info!(context, "Text unchanged."); + return Ok(()); + } + + save_text_edit_to_db(context, &mut original_msg, &new_text).await?; + + let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs + edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs + if original_msg.get_showpadlock() { + edit_msg.param.set_int(Param::GuaranteeE2ee, 1); + } + edit_msg + .param + .set(Param::TextEditFor, original_msg.rfc724_mid); + edit_msg.hidden = true; + send_msg(context, original_msg.chat_id, &mut edit_msg).await?; + Ok(()) +} + +pub(crate) async fn save_text_edit_to_db( + context: &Context, + original_msg: &mut Message, + new_text: &str, +) -> Result<()> { + original_msg.param.set_int(Param::IsEdited, 1); + context + .sql + .execute( + "UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?", + ( + new_text, + message::normalize_text(new_text), + original_msg.param.to_string(), + original_msg.id, + ), + ) + .await?; + context.emit_msgs_changed(original_msg.chat_id, original_msg.id); + Ok(()) +} + /// Sends invitation to a videochat. pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result { ensure!( @@ -3328,7 +3400,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> } else { start_chat_ephemeral_timers(context, chat_id).await?; - if context + let noticed_msgs_count = context .sql .execute( "UPDATE msgs @@ -3338,9 +3410,36 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> AND chat_id=?;", (MessageState::InNoticed, MessageState::InFresh, chat_id), ) - .await? - == 0 - { + .await?; + + // This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed + // locally (i.e. when the chat was opened locally). + let hidden_messages = context + .sql + .query_map( + "SELECT id, rfc724_mid FROM msgs + WHERE state=? + AND hidden=1 + AND chat_id=? + ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway + (MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed + |row| { + let msg_id: MsgId = row.get(0)?; + let rfc724_mid: String = row.get(1)?; + Ok((msg_id, rfc724_mid)) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; + for (msg_id, rfc724_mid) in &hidden_messages { + message::update_msg_state(context, *msg_id, MessageState::InSeen).await?; + imap::markseen_on_imap_table(context, rfc724_mid).await?; + } + + if noticed_msgs_count == 0 { return Ok(()); } } @@ -3435,37 +3534,62 @@ pub async fn get_chat_media( msg_type2: Viewtype, msg_type3: Viewtype, ) -> Result> { - // TODO This query could/should be converted to `AND type IN (?, ?, ?)`. - let list = context - .sql - .query_map( - "SELECT id + let list = if msg_type == Viewtype::Webxdc + && msg_type2 == Viewtype::Unknown + && msg_type3 == Viewtype::Unknown + { + context + .sql + .query_map( + "SELECT id FROM msgs WHERE (1=? OR chat_id=?) AND chat_id != ? - AND (type=? OR type=? OR type=?) + AND type = ? + AND hidden=0 + ORDER BY max(timestamp, timestamp_rcvd), id;", + ( + chat_id.is_none(), + chat_id.unwrap_or_else(|| ChatId::new(0)), + DC_CHAT_ID_TRASH, + Viewtype::Webxdc, + ), + |row| row.get::<_, MsgId>(0), + |ids| Ok(ids.flatten().collect()), + ) + .await? + } else { + context + .sql + .query_map( + "SELECT id + FROM msgs + WHERE (1=? OR chat_id=?) + AND chat_id != ? + AND type IN (?, ?, ?) AND hidden=0 ORDER BY timestamp, id;", - ( - chat_id.is_none(), - chat_id.unwrap_or_else(|| ChatId::new(0)), - DC_CHAT_ID_TRASH, - msg_type, - if msg_type2 != Viewtype::Unknown { - msg_type2 - } else { - msg_type - }, - if msg_type3 != Viewtype::Unknown { - msg_type3 - } else { - msg_type - }, - ), - |row| row.get::<_, MsgId>(0), - |ids| Ok(ids.flatten().collect()), - ) - .await?; + ( + chat_id.is_none(), + chat_id.unwrap_or_else(|| ChatId::new(0)), + DC_CHAT_ID_TRASH, + msg_type, + if msg_type2 != Viewtype::Unknown { + msg_type2 + } else { + msg_type + }, + if msg_type3 != Viewtype::Unknown { + msg_type3 + } else { + msg_type + }, + ), + |row| row.get::<_, MsgId>(0), + |ids| Ok(ids.flatten().collect()), + ) + .await? + }; Ok(list) } @@ -3493,6 +3617,8 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result Result> { let now = time(); let list = context @@ -3505,7 +3631,7 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul WHERE cc.chat_id=? AND cc.add_timestamp < cc.remove_timestamp AND ? < cc.remove_timestamp - ORDER BY c.id=1, c.last_seen DESC, c.id DESC", + ORDER BY c.id=1, cc.remove_timestamp DESC, c.id DESC", (chat_id, now.saturating_sub(60 * 24 * 3600)), |row| row.get::<_, ContactId>(0), |ids| ids.collect::, _>>().map_err(Into::into), @@ -3748,7 +3874,7 @@ pub(crate) async fn add_contact_to_chat_ex( ) -> Result { ensure!(!chat_id.is_special(), "can not add member to special chats"); let contact = Contact::get_by_id(context, contact_id).await?; - let mut msg = Message::default(); + let mut msg = Message::new(Viewtype::default()); chat_id.reset_gossiped_timestamp(context).await?; @@ -3779,7 +3905,9 @@ pub(crate) async fn add_contact_to_chat_ex( let sync_qr_code_tokens; if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { - chat.param.remove(Param::Unpromoted); + chat.param + .remove(Param::Unpromoted) + .set_i64(Param::GroupNameTimestamp, smeared_time(context)); chat.update_param(context).await?; sync_qr_code_tokens = true; } else { @@ -3822,6 +3950,8 @@ pub(crate) async fn add_contact_to_chat_ex( msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact_addr); msg.param.set_int(Param::Arg2, from_handshake.into()); + msg.param + .set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32); send_msg(context, chat_id, &mut msg).await?; sync = Nosync; @@ -3975,7 +4105,7 @@ pub async fn remove_contact_from_chat( "Cannot remove special contact" ); - let mut msg = Message::default(); + let mut msg = Message::new(Viewtype::default()); let chat = Chat::load_from_db(context, chat_id).await?; if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast { @@ -4019,6 +4149,8 @@ pub async fn remove_contact_from_chat( } msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); msg.param.set(Param::Arg, contact.get_addr().to_lowercase()); + msg.param + .set(Param::ContactAddedRemoved, contact.id.to_u32() as i32); let res = send_msg(context, chat_id, &mut msg).await; if contact_id == ContactId::SELF { res?; @@ -4080,7 +4212,7 @@ async fn rename_ex( ensure!(!chat_id.is_special(), "Invalid chat ID"); let chat = Chat::load_from_db(context, chat_id).await?; - let mut msg = Message::default(); + let mut msg = Message::new(Viewtype::default()); if chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist @@ -4236,15 +4368,18 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) msg.param.remove(Param::WebxdcDocumentTimestamp); msg.param.remove(Param::WebxdcSummary); msg.param.remove(Param::WebxdcSummaryTimestamp); + msg.param.remove(Param::IsEdited); msg.in_reply_to = None; // do not leak data as group names; a default subject is generated by mimefactory msg.subject = "".to_string(); msg.state = MessageState::OutPending; + msg.rfc724_mid = create_outgoing_rfc724_mid(); let new_msg_id = chat .prepare_msg_raw(context, &mut msg, None, curr_timestamp) .await?; + curr_timestamp += 1; if !create_send_msg_jobs(context, &mut msg).await?.is_empty() { context.scheduler.interrupt_smtp().await; @@ -4270,7 +4405,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { }) .await?; } - context.send_sync_msg().await?; + context.scheduler.interrupt_inbox().await; Ok(()) } @@ -4296,7 +4431,7 @@ pub(crate) async fn save_copy_in_self_talk( bail!("message already saved."); } - let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, txt_raw, \ + let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \ mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg"; let row_id = context .sql @@ -4490,17 +4625,7 @@ pub async fn add_device_msg_with_importance( // makes sure, the added message is the last one, // even if the date is wrong (useful esp. when warning about bad dates) let mut timestamp_sort = timestamp_sent; - if let Some(last_msg_time) = context - .sql - .query_get_value( - "SELECT MAX(timestamp) - FROM msgs - WHERE chat_id=? - HAVING COUNT(*) > 0", - (chat_id,), - ) - .await? - { + if let Some(last_msg_time) = chat_id.get_timestamp(context).await? { if timestamp_sort <= last_msg_time { timestamp_sort = last_msg_time + 1; } @@ -4627,13 +4752,17 @@ pub(crate) async fn add_info_msg_with_cmd( timestamp_sent_rcvd: Option, parent: Option<&Message>, from_id: Option, + added_removed_id: Option, ) -> Result { let rfc724_mid = create_outgoing_rfc724_mid(); let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?; let mut param = Params::new(); if cmd != SystemMessage::Unknown { - param.set_cmd(cmd) + param.set_cmd(cmd); + } + if let Some(contact_id) = added_removed_id { + param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string()); } let row_id = @@ -4681,6 +4810,7 @@ pub(crate) async fn add_info_msg( None, None, None, + None, ) .await } @@ -4748,6 +4878,9 @@ pub(crate) enum SyncId { Grpid(String), /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups. Msgids(Vec), + + // Special id for device chat. + Device, } /// An action synchronised to other devices. @@ -4763,6 +4896,7 @@ pub(crate) enum SyncAction { Rename(String), /// Set chat contacts by their addresses. SetContacts(Vec), + Delete, } impl Context { @@ -4809,6 +4943,7 @@ impl Context { ChatId::lookup_by_message(&msg) .with_context(|| format!("No chat found for Message-IDs {msgids:?}"))? } + SyncId::Device => ChatId::get_for_contact(self, ContactId::DEVICE).await?, }; match action { SyncAction::Block => chat_id.block_ex(self, Nosync).await, @@ -4821,6 +4956,7 @@ impl Context { } SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await, SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await, + SyncAction::Delete => chat_id.delete_ex(self, Nosync).await, } } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 2b1a2ea95f..2f75d7c00d 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::chatlist::get_archived_cnt; use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; +use crate::ephemeral::Timer; use crate::headerdef::HeaderDef; use crate::imex::{has_backup, imex, ImexMode}; use crate::message::{delete_msgs, MessengerMessage}; @@ -298,13 +299,11 @@ async fn test_member_add_remove() -> Result<()> { let alice = tcm.alice().await; let bob = tcm.bob().await; - - // Disable encryption so we can inspect raw message contents. - alice.set_config(Config::E2eeEnabled, Some("0")).await?; - bob.set_config(Config::E2eeEnabled, Some("0")).await?; + let fiona = tcm.fiona().await; // Create contact for Bob on the Alice side with name "robert". - let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await; + alice_bob_contact_id.set_name(&alice, "robert").await?; // Set Bob authname to "Bob" and send it to Alice. bob.set_config(Config::Displayname, Some("Bob")).await?; @@ -324,42 +323,35 @@ async fn test_member_add_remove() -> Result<()> { // Create and promote a group. let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?; - let sent = alice + alice .send_text(alice_chat_id, "Hi! I created a group.") .await; - assert!(sent.payload.contains("Hi! I created a group.")); // Alice adds Bob to the chat. add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent = alice.pop_sent_msg().await; - assert!(sent - .payload - .contains("I added member Bob (bob@example.net).")); + // Locally set name "robert" should not leak. assert!(!sent.payload.contains("robert")); assert_eq!( sent.load_from_db().await.get_text(), - "You added member robert (bob@example.net)." + "You added member robert." ); // Alice removes Bob from the chat. remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; let sent = alice.pop_sent_msg().await; - assert!(sent - .payload - .contains("I removed member Bob (bob@example.net).")); assert!(!sent.payload.contains("robert")); assert_eq!( sent.load_from_db().await.get_text(), - "You removed member robert (bob@example.net)." + "You removed member robert." ); // Alice leaves the chat. remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; let sent = alice.pop_sent_msg().await; - assert!(sent.payload.contains("I left the group.")); assert_eq!(sent.load_from_db().await.get_text(), "You left the group."); Ok(()) @@ -372,13 +364,12 @@ async fn test_parallel_member_remove() -> Result<()> { let alice = tcm.alice().await; let bob = tcm.bob().await; + let charlie = tcm.charlie().await; + let fiona = tcm.fiona().await; - alice.set_config(Config::E2eeEnabled, Some("0")).await?; - bob.set_config(Config::E2eeEnabled, Some("0")).await?; - - let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?; - let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; + let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await; // Create and promote a group. let alice_chat_id = @@ -393,8 +384,8 @@ async fn test_parallel_member_remove() -> Result<()> { let bob_chat_id = bob_received_msg.get_chat_id(); bob_chat_id.accept(&bob).await?; - // Alice adds Claire to the chat. - add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?; + // Alice adds Charlie to the chat. + add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?; let alice_sent_add_msg = alice.pop_sent_msg().await; // Bob leaves the chat. @@ -423,7 +414,7 @@ async fn test_parallel_member_remove() -> Result<()> { // Test that remove message is rewritten. assert_eq!( bob_received_remove_msg.get_text(), - "Member Me (bob@example.net) removed by alice@example.org." + "Member Me removed by alice@example.org." ); Ok(()) @@ -435,11 +426,10 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; - let alice_bob_contact_id = - Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?; - let fiona_addr = "fiona@example.net"; - let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?; - let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?; + let fiona = tcm.fiona().await; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; + let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?; add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; @@ -481,8 +471,9 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_modify_chat_multi_device() -> Result<()> { - let a1 = TestContext::new_alice().await; - let a2 = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let a1 = tcm.alice().await; + let a2 = tcm.alice().await; a1.set_config_bool(Config::BccSelf, true).await?; // create group and sync it to the second device @@ -506,8 +497,9 @@ async fn test_modify_chat_multi_device() -> Result<()> { assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1); // add a member to the group - let bob = Contact::create(&a1, "", "bob@example.org").await?; - add_contact_to_chat(&a1, a1_chat_id, bob).await?; + let bob = tcm.bob().await; + let bob_id = a1.add_or_lookup_contact_id(&bob).await; + add_contact_to_chat(&a1, a1_chat_id, bob_id).await?; let a1_msg = a1.get_last_msg().await; let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; @@ -531,11 +523,19 @@ async fn test_modify_chat_multi_device() -> Result<()> { assert!(a2_msg.is_system_message()); assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged); assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged); + assert_eq!( + a1_msg.get_info_contact_id(&a1).await?, + Some(ContactId::SELF) + ); + assert_eq!( + a2_msg.get_info_contact_id(&a2).await?, + Some(ContactId::SELF) + ); assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar"); assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar"); // remove member from group - remove_contact_from_chat(&a1, a1_chat_id, bob).await?; + remove_contact_from_chat(&a1, a1_chat_id, bob_id).await?; let a1_msg = a1.get_last_msg().await; let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await; @@ -562,13 +562,18 @@ async fn test_modify_chat_multi_device() -> Result<()> { async fn test_modify_chat_disordered() -> Result<()> { let _n = TimeShiftFalsePositiveNote; - // Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy + let mut tcm = TestContextManager::new(); + + // Alice creates a group with Bob, Charlie and Fiona and then removes Charlie and Fiona // (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then) - let alice = TestContext::new_alice().await; + let alice = tcm.alice().await; - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; - let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; + let bob = tcm.bob().await; + let bob_id = alice.add_or_lookup_contact_id(&bob).await; + let charlie = tcm.charlie().await; + let charlie_id = alice.add_or_lookup_contact_id(&charlie).await; + let fiona = tcm.fiona().await; + let fiona_id = alice.add_or_lookup_contact_id(&fiona).await; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; @@ -576,21 +581,21 @@ async fn test_modify_chat_disordered() -> Result<()> { add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; let add1 = alice.pop_sent_msg().await; - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; + add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?; let add2 = alice.pop_sent_msg().await; SystemTime::shift(Duration::from_millis(1100)); - add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; + add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?; let add3 = alice.pop_sent_msg().await; SystemTime::shift(Duration::from_millis(1100)); assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4); - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?; let remove1 = alice.pop_sent_msg().await; SystemTime::shift(Duration::from_millis(1100)); - remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; + remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?; let remove2 = alice.pop_sent_msg().await; assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); @@ -641,24 +646,27 @@ async fn test_modify_chat_lost() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "", "claire@foo.de").await?; - let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?; + let bob = tcm.bob().await; + let bob_id = alice.add_or_lookup_contact_id(&bob).await; + let charlie = tcm.charlie().await; + let charlie_id = alice.add_or_lookup_contact_id(&charlie).await; + let fiona = tcm.fiona().await; + let fiona_id = alice.add_or_lookup_contact_id(&fiona).await; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; - add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?; + add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?; + add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?; send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; let add = alice.pop_sent_msg().await; SystemTime::shift(Duration::from_millis(1100)); - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?; let remove1 = alice.pop_sent_msg().await; SystemTime::shift(Duration::from_millis(1100)); - remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?; + remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?; let remove2 = alice.pop_sent_msg().await; let bob = tcm.bob().await; @@ -681,12 +689,13 @@ async fn test_modify_chat_lost() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_leave_group() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; // Create group chat with Bob. let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - let bob_contact = Contact::create(&alice, "", "bob@example.net").await?; + let bob_contact = alice.add_or_lookup_contact(&bob).await.id; add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?; // Alice sends first message to group. @@ -695,16 +704,45 @@ async fn test_leave_group() -> Result<()> { assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); + // Clear events so that we can later check + // that the 'Group left' message didn't trigger IncomingMsg: + alice.evtracker.clear_events(); + + // Shift the time so that we can later check the 'Group left' message's timestamp: + SystemTime::shift(Duration::from_secs(60)); + // Bob leaves the group. let bob_chat_id = bob_msg.chat_id; bob_chat_id.accept(&bob).await?; remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; let leave_msg = bob.pop_sent_msg().await; - alice.recv_msg(&leave_msg).await; + let rcvd_leave_msg = alice.recv_msg(&leave_msg).await; assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1); + assert_eq!(rcvd_leave_msg.state, MessageState::InSeen); + + alice.emit_event(EventType::Test); + alice + .evtracker + .get_matching(|ev| match ev { + EventType::Test => true, + EventType::IncomingMsg { .. } => panic!("'Group left' message should be silent"), + EventType::MsgsNoticed(..) => { + panic!("'Group left' message shouldn't clear notifications") + } + _ => false, + }) + .await; + + // The 'Group left' message timestamp should be the same as the previous message in the chat + // so that the chat is not popped up in the chatlist: + assert_eq!( + sent_msg.load_from_db().await.timestamp_sort, + rcvd_leave_msg.timestamp_sort + ); + Ok(()) } @@ -774,6 +812,21 @@ async fn test_self_talk() -> Result<()> { Ok(()) } +/// Tests that when BCC-self is disabled +/// and no messages are actually sent +/// in a self-chat, they have a padlock. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_talk_no_bcc_padlock() -> Result<()> { + let t = &TestContext::new_alice().await; + t.set_config_bool(Config::BccSelf, false).await?; + let chat = &t.get_self_chat().await; + + let msg_id = send_text_msg(t, chat.id, "Foobar".to_string()).await?; + let msg = Message::load_from_db(t, msg_id).await?; + assert!(msg.get_showpadlock()); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_device_msg_unlabelled() { let t = TestContext::new().await; @@ -1355,20 +1408,52 @@ async fn test_pinned_after_new_msgs() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_set_chat_name() { - let t = TestContext::new().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo") .await .unwrap(); assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), + Chat::load_from_db(alice, chat_id).await.unwrap().get_name(), "foo" ); - set_chat_name(&t, chat_id, "bar").await.unwrap(); + set_chat_name(alice, chat_id, "bar").await.unwrap(); assert_eq!( - Chat::load_from_db(&t, chat_id).await.unwrap().get_name(), + Chat::load_from_db(alice, chat_id).await.unwrap().get_name(), "bar" ); + + let bob = &tcm.bob().await; + let bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + add_contact_to_chat(alice, chat_id, bob_contact_id) + .await + .unwrap(); + + let sent_msg = alice.send_text(chat_id, "Hi").await; + let received_msg = bob.recv_msg(&sent_msg).await; + let bob_chat_id = received_msg.chat_id; + + for new_name in [ + "Baz", + "xyzzy", + "Quux", + "another name", + "something different", + ] { + set_chat_name(alice, chat_id, new_name).await.unwrap(); + let sent_msg = alice.pop_sent_msg().await; + let received_msg = bob.recv_msg(&sent_msg).await; + assert_eq!(received_msg.chat_id, bob_chat_id); + assert_eq!( + Chat::load_from_db(bob, bob_chat_id) + .await + .unwrap() + .get_name(), + new_name + ); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1418,6 +1503,39 @@ async fn test_shall_attach_selfavatar() -> Result<()> { Ok(()) } +/// Tests that profile data is attached to group leave messages. There are some pros and cons of +/// doing this, but at least we don't want to complicate the code with this special case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_profile_data_on_group_leave() -> Result<()> { + let mut tcm = TestContextManager::new(); + let t = &tcm.alice().await; + let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?; + + let (contact_id, _) = Contact::add_or_lookup( + t, + "", + &ContactAddress::new("foo@bar.org")?, + Origin::IncomingUnknownTo, + ) + .await?; + add_contact_to_chat(t, chat_id, contact_id).await?; + + send_text_msg(t, chat_id, "populate".to_string()).await?; + t.pop_sent_msg().await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + assert!(shall_attach_selfavatar(t, chat_id).await?); + + remove_contact_from_chat(t, chat_id, ContactId::SELF).await?; + let sent_msg = t.pop_sent_msg().await; + assert!(sent_msg.payload().contains("Chat-User-Avatar")); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_set_mute_duration() { let t = TestContext::new().await; @@ -1499,6 +1617,7 @@ async fn test_add_info_msg_with_cmd() -> Result<()> { None, None, None, + None, ) .await?; @@ -1578,58 +1697,6 @@ async fn test_lookup_self_by_contact_id() { assert_eq!(chat.blocked, Blocked::Not); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_group_with_removed_message_id() -> Result<()> { - // Alice creates a group with Bob, sends a message to bob - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let alice_bob_contact = alice.add_or_lookup_contact(&bob).await; - let contact_id = alice_bob_contact.id; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; - - add_contact_to_chat(&alice, alice_chat_id, contact_id).await?; - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2); - send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?; - assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1); - - // 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: <").count(), 2); - assert_eq!(msg.match_indices("References: <").count(), 1); - let msg = msg.replace("Message-ID: <", "Message-ID: Result<()> { let t = TestContext::new_alice().await; @@ -1839,10 +1906,6 @@ async fn test_sticker( msg.set_file_and_deduplicate(&alice, &file, Some(filename), None)?; let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; - let mime = sent_msg.payload(); - if res_viewtype == Viewtype::Sticker { - assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1); - } let msg = bob.recv_msg(&sent_msg).await; assert_eq!(msg.chat_id, bob_chat.id); @@ -1977,20 +2040,28 @@ async fn test_sticker_forward() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_forward() -> Result<()> { +async fn test_forward_basic() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let alice_chat = alice.create_chat(&bob).await; let bob_chat = bob.create_chat(&alice).await; - let mut msg = Message::new_text("Hi Bob".to_owned()); - let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; + let mut alice_msg = Message::new_text("Hi Bob".to_owned()); + let sent_msg = alice.send_msg(alice_chat.get_id(), &mut alice_msg).await; let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(alice_msg.rfc724_mid, msg.rfc724_mid); forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?; let forwarded_msg = bob.pop_sent_msg().await; + assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2); + assert_ne!( + forwarded_msg.load_from_db().await.rfc724_mid, + msg.rfc724_mid, + ); + let msg_bob = Message::load_from_db(&bob, forwarded_msg.sender_msg_id).await?; let msg = alice.recv_msg(&forwarded_msg).await; + assert_eq!(msg.rfc724_mid(), msg_bob.rfc724_mid()); assert_eq!(msg.get_text(), "Hi Bob"); assert!(msg.is_forwarded()); Ok(()) @@ -1998,20 +2069,22 @@ async fn test_forward() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_forward_info_msg() -> Result<()> { - let t = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?; - send_text_msg(&t, chat_id1, "msg one".to_string()).await?; - let bob_id = Contact::create(&t, "", "bob@example.net").await?; - add_contact_to_chat(&t, chat_id1, bob_id).await?; - let msg1 = t.get_last_msg_in(chat_id1).await; + let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?; + send_text_msg(alice, chat_id1, "msg one".to_string()).await?; + let bob_id = alice.add_or_lookup_contact_id(bob).await; + add_contact_to_chat(alice, chat_id1, bob_id).await?; + let msg1 = alice.get_last_msg_in(chat_id1).await; assert!(msg1.is_info()); assert!(msg1.get_text().contains("bob@example.net")); - let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?; - assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0); - forward_msgs(&t, &[msg1.id], chat_id2).await?; - let msg2 = t.get_last_msg_in(chat_id2).await; + let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?; + assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0); + forward_msgs(alice, &[msg1.id], chat_id2).await?; + let msg2 = alice.get_last_msg_in(chat_id2).await; assert!(!msg2.is_info()); // forwarded info-messages lose their info-state assert_eq!(msg2.get_info_type(), SystemMessage::Unknown); assert_ne!(msg2.from_id, ContactId::INFO); @@ -2058,8 +2131,10 @@ async fn test_forward_quote() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_forward_group() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let charlie = tcm.charlie().await; let alice_chat = alice.create_chat(&bob).await; let bob_chat = bob.create_chat(&alice).await; @@ -2067,12 +2142,12 @@ async fn test_forward_group() -> Result<()> { // Alice creates a group with Bob. let alice_group_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?; + let bob_id = alice.add_or_lookup_contact_id(&bob).await; + let charlie_id = alice.add_or_lookup_contact_id(&charlie).await; add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?; + add_contact_to_chat(&alice, alice_group_chat_id, charlie_id).await?; let sent_group_msg = alice - .send_text(alice_group_chat_id, "Hi Bob and Claire") + .send_text(alice_group_chat_id, "Hi Bob and Charlie") .await; let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id; @@ -2260,6 +2335,29 @@ async fn test_forward_from_saved_to_saved() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_encrypted_to_unencrypted() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + let txt = "This should be encrypted"; + let sent = alice.send_text(alice.create_chat(bob).await.id, txt).await; + let msg = bob.recv_msg(&sent).await; + assert_eq!(msg.text, txt); + assert!(msg.get_showpadlock()); + + let unencrypted_chat = bob.create_email_chat(charlie).await; + forward_msgs(bob, &[msg.id], unencrypted_chat.id).await?; + let msg2 = bob.get_last_msg().await; + assert_eq!(msg2.text, txt); + assert_ne!(msg.id, msg2.id); + assert!(!msg2.get_showpadlock()); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_save_from_saved_to_saved_failing() -> Result<()> { let alice = TestContext::new_alice().await; @@ -2281,11 +2379,13 @@ async fn test_save_from_saved_to_saved_failing() -> Result<()> { async fn test_resend_own_message() -> Result<()> { // Alice creates group with Bob and sends an initial message let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let fiona = TestContext::new_fiona().await; let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; add_contact_to_chat( &alice, alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, + alice.add_or_lookup_contact_id(&bob).await, ) .await?; let sent1 = alice.send_text(alice_grp, "alice->bob").await; @@ -2294,7 +2394,7 @@ async fn test_resend_own_message() -> Result<()> { add_contact_to_chat( &alice, alice_grp, - Contact::create(&alice, "", "claire@example.org").await?, + alice.add_or_lookup_contact_id(&fiona).await, ) .await?; let sent2 = alice.pop_sent_msg().await; @@ -2338,15 +2438,13 @@ async fn test_resend_own_message() -> Result<()> { assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3); assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2); - // Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob - let claire = TestContext::new().await; - claire.configure_addr("claire@example.org").await; - claire.recv_msg(&sent2).await; - let msg = claire.recv_msg(&sent3).await; + // Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob + fiona.recv_msg(&sent2).await; + let msg = fiona.recv_msg(&sent3).await; assert_eq!(msg.get_text(), "alice->bob"); - assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3); - assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2); - let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?; + assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3); + assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2); + let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?; assert_eq!(msg_from.get_addr(), "alice@example.org"); assert!(sent1_ts_sent < msg.timestamp_sent); @@ -2355,19 +2453,15 @@ async fn test_resend_own_message() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_resend_foreign_message_fails() -> Result<()> { - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?; let sent1 = alice.send_text(alice_grp, "alice->bob").await; - let bob = TestContext::new_bob().await; let msg = bob.recv_msg(&sent1).await; - assert!(resend_msgs(&bob, &[msg.id]).await.is_err()); + assert!(resend_msgs(bob, &[msg.id]).await.is_err()); Ok(()) } @@ -2411,24 +2505,23 @@ async fn test_resend_opportunistically_encryption() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_resend_info_message_fails() -> Result<()> { - let alice = TestContext::new_alice().await; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; + add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?; alice.send_text(alice_grp, "alice->bob").await; add_contact_to_chat( - &alice, + alice, alice_grp, - Contact::create(&alice, "", "claire@example.org").await?, + alice.add_or_lookup_contact_id(charlie).await, ) .await?; let sent2 = alice.pop_sent_msg().await; - assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err()); + assert!(resend_msgs(alice, &[sent2.sender_msg_id]).await.is_err()); Ok(()) } @@ -2464,6 +2557,7 @@ async fn test_broadcast() -> Result<()> { // create two context, send two messages so both know the other let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; + let fiona = TestContext::new_fiona().await; let chat_alice = alice.create_chat(&bob).await; send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; @@ -2482,6 +2576,8 @@ async fn test_broadcast() -> Result<()> { get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(), ) .await?; + let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await; + add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?; set_chat_name(&alice, broadcast_id, "Broadcast list").await?; { let chat = Chat::load_from_db(&alice, broadcast_id).await?; @@ -2496,11 +2592,14 @@ async fn test_broadcast() -> Result<()> { { let sent_msg = alice.pop_sent_msg().await; - assert!(!sent_msg.payload.contains("Chat-Group-Member-Timestamps:")); + let msg = bob.parse_msg(&sent_msg).await; + assert!(msg.was_encrypted()); + assert!(!msg.header_exists(HeaderDef::ChatGroupMemberTimestamps)); + assert!(!msg.header_exists(HeaderDef::AutocryptGossip)); let msg = bob.recv_msg(&sent_msg).await; assert_eq!(msg.get_text(), "ola!"); assert_eq!(msg.subject, "Broadcast list"); - assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data + assert!(msg.get_showpadlock()); let chat = Chat::load_from_db(&bob, msg.chat_id).await?; assert_eq!(chat.typ, Chattype::Mailinglist); assert_ne!(chat.id, chat_bob.id); @@ -2637,11 +2736,10 @@ async fn test_chat_get_encryption_info() -> Result<()> { "No encryption:\n\ fiona@example.net\n\ \n\ - End-to-end encryption preferred:\n\ + End-to-end encryption available:\n\ bob@example.net" ); - bob.set_config(Config::E2eeEnabled, Some("0")).await?; send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?; alice.recv_msg(&bob.pop_sent_msg().await).await; @@ -2826,17 +2924,67 @@ async fn test_get_chat_media() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_chat_media_webxdc_order() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat = alice.create_chat(&bob).await; + + let mut instance1 = Message::new(Viewtype::Webxdc); + instance1.set_file_from_bytes( + &alice, + "test1.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + None, + )?; + let instance1_id = send_msg(&alice, chat.id, &mut instance1).await?; + + let mut instance2 = Message::new(Viewtype::Webxdc); + instance2.set_file_from_bytes( + &alice, + "test2.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + None, + )?; + let instance2_id = send_msg(&alice, chat.id, &mut instance2).await?; + + // list is ordered oldest to newest, check that + let media = get_chat_media( + &alice, + Some(chat.id), + Viewtype::Webxdc, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await?; + assert_eq!(media.first().unwrap(), &instance1_id); + assert_eq!(media.get(1).unwrap(), &instance2_id); + + // add a status update for the oder instance; that resorts the list + alice + .send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#) + .await?; + let media = get_chat_media( + &alice, + Some(chat.id), + Viewtype::Webxdc, + Viewtype::Unknown, + Viewtype::Unknown, + ) + .await?; + assert_eq!(media.first().unwrap(), &instance2_id); + assert_eq!(media.get(1).unwrap(), &instance1_id); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_blob_renaming() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - add_contact_to_chat( - &alice, - chat_id, - Contact::create(&alice, "bob", "bob@example.net").await?, - ) - .await?; + add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?; let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe"); fs::write(&file, "aaa").await?; let mut msg = Message::new(Viewtype::File); @@ -2868,7 +3016,7 @@ async fn test_sync_blocked() -> Result<()> { let sent_msg = bob.send_text(ba_chat.id, "hi").await; let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; alice1.recv_msg(&sent_msg).await; - let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id; + let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await; assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request); a0b_chat_id.accept(alice0).await?; @@ -2923,26 +3071,36 @@ async fn test_sync_blocked() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_accept_before_first_msg() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + 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 bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let ba_chat = bob.create_chat(alice0).await; let sent_msg = bob.send_text(ba_chat.id, "hi").await; - let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); + let rcvd_msg = alice0.recv_msg(&sent_msg).await; + let a0b_chat_id = rcvd_msg.chat_id; + let a0b_contact_id = rcvd_msg.from_id; + assert_eq!( + Chat::load_from_db(alice0, a0b_chat_id).await?.blocked, + Blocked::Request + ); a0b_chat_id.accept(alice0).await?; - let a0b_contact = alice0.add_or_lookup_contact(&bob).await; + let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::CreateChat); - assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not); + assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not); sync(alice0, alice1).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; + let alice1_contacts = Contact::get_all(alice1, 0, None).await?; + assert_eq!(alice1_contacts.len(), 1); + let a1b_contact_id = alice1_contacts[0]; + let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; + assert_eq!(a1b_contact.get_addr(), "bob@example.net"); assert_eq!(a1b_contact.origin, Origin::CreateChat); - let a1b_chat = alice1.get_chat(&bob).await; + let a1b_chat = alice1.get_chat(bob).await; assert_eq!(a1b_chat.blocked, Blocked::Not); let chats = Chatlist::try_load(alice1, 0, None, None).await?; assert_eq!(chats.len(), 1); @@ -2963,22 +3121,22 @@ async fn test_sync_block_before_first_msg() -> Result<()> { let ba_chat = bob.create_chat(alice0).await; let sent_msg = bob.send_text(ba_chat.id, "hi").await; - let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + let rcvd_msg = alice0.recv_msg(&sent_msg).await; + let a0b_chat_id = rcvd_msg.chat_id; + let a0b_contact_id = rcvd_msg.from_id; assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request); a0b_chat_id.block(alice0).await?; - let a0b_contact = alice0.add_or_lookup_contact(&bob).await; + let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?; assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom); assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes); sync(alice0, alice1).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(a1b_contact.origin, Origin::Hidden); - assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id) - .await? - .is_none()); + let alice1_contacts = Contact::get_all(alice1, 0, None).await?; + assert_eq!(alice1_contacts.len(), 0); let rcvd_msg = alice1.recv_msg(&sent_msg).await; - let a1b_contact = alice1.add_or_lookup_contact(&bob).await; + let a1b_contact_id = rcvd_msg.from_id; + let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom); let a1b_chat = alice1.get_chat(&bob).await; assert_eq!(a1b_chat.blocked, Blocked::Yes); @@ -2986,6 +3144,48 @@ async fn test_sync_block_before_first_msg() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_delete_chat() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + let bob = TestContext::new_bob().await; + + let ba_chat = bob.create_chat(alice0).await; + let sent_msg = bob.send_text(ba_chat.id, "hi").await; + let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + let a1b_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; + a0b_chat_id.accept(alice0).await?; + sync(alice0, alice1).await; + a0b_chat_id.delete(alice0).await?; + sync(alice0, alice1).await; + alice1.assert_no_chat(a1b_chat_id).await; + alice1 + .evtracker + .get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. })) + .await; + + let bob_grp_chat_id = bob + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0]) + .await; + let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await; + let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id; + let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id; + a0_grp_chat_id.accept(alice0).await?; + sync(alice0, alice1).await; + a0_grp_chat_id.delete(alice0).await?; + sync(alice0, alice1).await; + alice1.assert_no_chat(a1_grp_chat_id).await; + alice0 + .evtracker + .get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. })) + .await; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_adhoc_grp() -> Result<()> { let alice0 = &TestContext::new_alice().await; @@ -3036,8 +3236,9 @@ async fn test_sync_adhoc_grp() -> Result<()> { /// - That sync messages don't unarchive the self-chat. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_visibility() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + 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?; } @@ -3059,10 +3260,39 @@ async fn test_sync_visibility() -> Result<()> { Ok(()) } +/// Tests syncing of chat visibility on device message chat. +/// +/// Previously due to a bug pinning "Device Messages" +/// chat resulted in creation of `device@localhost` chat +/// on another device. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_device_messages_visibility() -> 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 device_chat_id0 = ChatId::get_for_contact(alice0, ContactId::DEVICE).await?; + device_chat_id0 + .set_visibility(alice0, ChatVisibility::Pinned) + .await?; + + sync(alice0, alice1).await; + + let device_chat_id1 = ChatId::get_for_contact(alice1, ContactId::DEVICE).await?; + let device_chat1 = Chat::load_from_db(alice1, device_chat_id1).await?; + assert_eq!(device_chat1.get_visibility(), ChatVisibility::Pinned); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_muted() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + 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?; } @@ -3096,8 +3326,9 @@ async fn test_sync_muted() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_broadcast() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; + 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?; } @@ -3140,6 +3371,10 @@ async fn test_sync_broadcast() -> Result<()> { assert!(get_past_chat_contacts(alice1, a1_broadcast_id) .await? .is_empty()); + + a0_broadcast_id.delete(alice0).await?; + sync(alice0, alice1).await; + alice1.assert_no_chat(a1_broadcast_id).await; Ok(()) } @@ -3229,6 +3464,128 @@ async fn test_do_not_overwrite_draft() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_info_contact_id() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + + async fn pop_recv_and_check( + alice: &TestContext, + alice2: &TestContext, + bob: &TestContext, + expected_type: SystemMessage, + expected_alice_id: ContactId, + expected_bob_id: ContactId, + ) -> Result<()> { + let sent_msg = alice.pop_sent_msg().await; + let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?; + assert_eq!(msg.get_info_type(), expected_type); + assert_eq!( + msg.get_info_contact_id(alice).await?, + Some(expected_alice_id) + ); + + let msg = alice2.recv_msg(&sent_msg).await; + assert_eq!(msg.get_info_type(), expected_type); + assert_eq!( + msg.get_info_contact_id(alice2).await?, + Some(expected_alice_id) + ); + + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_info_type(), expected_type); + assert_eq!(msg.get_info_contact_id(bob).await?, Some(expected_bob_id)); + + Ok(()) + } + + // Alice creates group, Bob receives group + let alice_chat_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob]) + .await; + let sent_msg1 = alice.send_text(alice_chat_id, "moin").await; + + let msg = bob.recv_msg(&sent_msg1).await; + let bob_alice_id = msg.from_id; + assert!(!bob_alice_id.is_special()); + + // Alice does group changes, Bob receives them + set_chat_name(alice, alice_chat_id, "games").await?; + pop_recv_and_check( + alice, + alice2, + bob, + SystemMessage::GroupNameChanged, + ContactId::SELF, + bob_alice_id, + ) + .await?; + + let file = alice.get_blobdir().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?; + pop_recv_and_check( + alice, + alice2, + bob, + SystemMessage::GroupImageChanged, + ContactId::SELF, + bob_alice_id, + ) + .await?; + + alice_chat_id + .set_ephemeral_timer(alice, Timer::Enabled { duration: 60 }) + .await?; + pop_recv_and_check( + alice, + alice2, + bob, + SystemMessage::EphemeralTimerChanged, + ContactId::SELF, + bob_alice_id, + ) + .await?; + + let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere + add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; + pop_recv_and_check( + alice, + alice2, + bob, + SystemMessage::MemberAddedToGroup, + fiona_id, + fiona_id, + ) + .await?; + + remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?; + pop_recv_and_check( + alice, + alice2, + bob, + SystemMessage::MemberRemovedFromGroup, + fiona_id, + fiona_id, + ) + .await?; + + // When fiona_id is deleted, get_info_contact_id() returns None. + // We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then) + alice + .sql + .execute("DELETE FROM contacts WHERE id=?", (fiona_id,)) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup); + assert!(msg.get_info_contact_id(alice).await?.is_none()); + + Ok(()) +} + /// Test group consistency. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_member_bug() -> Result<()> { @@ -3236,9 +3593,10 @@ async fn test_add_member_bug() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; - let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; // Create a group. let alice_chat_id = @@ -3282,7 +3640,8 @@ async fn test_past_members() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + let fiona = &tcm.fiona().await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; @@ -3294,8 +3653,7 @@ async fn test_past_members() -> Result<()> { assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1); let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; let add_message = alice.pop_sent_msg().await; @@ -3314,8 +3672,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; @@ -3327,7 +3684,8 @@ async fn non_member_cannot_modify_member_list() -> Result<()> { let bob_chat_id = bob_received_msg.get_chat_id(); bob_chat_id.accept(bob).await?; - let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?; + let fiona = &tcm.fiona().await; + let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await; // Alice removes Bob and Bob adds Fiona at the same time. remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?; @@ -3349,11 +3707,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; - let fiona_addr = "fiona@example.net"; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; @@ -3369,10 +3726,6 @@ async fn unpromoted_group_no_tombstones() -> Result<()> { assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); let sent = alice.send_text(alice_chat_id, "Hello group!").await; - let payload = sent.payload(); - assert_eq!(payload.contains("Hello group!"), true); - assert_eq!(payload.contains(&bob_addr), true); - assert_eq!(payload.contains(fiona_addr), false); let bob_msg = bob.recv_msg(&sent).await; let bob_chat_id = bob_msg.chat_id; @@ -3388,8 +3741,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let fiona_addr = "fiona@example.net"; - let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?; + let fiona = &tcm.fiona().await; + let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; @@ -3404,12 +3757,10 @@ async fn test_expire_past_members_after_60_days() -> Result<()> { assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; let add_message = alice.pop_sent_msg().await; - assert_eq!(add_message.payload.contains(fiona_addr), false); let bob_add_message = bob.recv_msg(&add_message).await; let bob_chat_id = bob_add_message.chat_id; assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); @@ -3418,6 +3769,65 @@ async fn test_expire_past_members_after_60_days() -> Result<()> { Ok(()) } +/// Test that past members are ordered by the timestamp of their removal. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_past_members_order() -> Result<()> { + let mut tcm = TestContextManager::new(); + let t = &tcm.alice().await; + + let bob = tcm.bob().await; + let bob_contact_id = t.add_or_lookup_contact_id(&bob).await; + let charlie = tcm.charlie().await; + let charlie_contact_id = t.add_or_lookup_contact_id(&charlie).await; + let fiona = tcm.fiona().await; + let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await; + + let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?; + add_contact_to_chat(t, chat_id, bob_contact_id).await?; + add_contact_to_chat(t, chat_id, charlie_contact_id).await?; + add_contact_to_chat(t, chat_id, fiona_contact_id).await?; + t.send_text(chat_id, "Hi! I created a group.").await; + + assert_eq!(get_past_chat_contacts(t, chat_id).await?.len(), 0); + + remove_contact_from_chat(t, chat_id, charlie_contact_id).await?; + + let past_contacts = get_past_chat_contacts(t, chat_id).await?; + assert_eq!(past_contacts.len(), 1); + assert_eq!(past_contacts[0], charlie_contact_id); + + SystemTime::shift(Duration::from_secs(5)); + remove_contact_from_chat(t, chat_id, bob_contact_id).await?; + + let past_contacts = get_past_chat_contacts(t, chat_id).await?; + assert_eq!(past_contacts.len(), 2); + assert_eq!(past_contacts[0], bob_contact_id); + assert_eq!(past_contacts[1], charlie_contact_id); + + SystemTime::shift(Duration::from_secs(5)); + remove_contact_from_chat(t, chat_id, fiona_contact_id).await?; + + let past_contacts = get_past_chat_contacts(t, chat_id).await?; + assert_eq!(past_contacts.len(), 3); + assert_eq!(past_contacts[0], fiona_contact_id); + assert_eq!(past_contacts[1], bob_contact_id); + assert_eq!(past_contacts[2], charlie_contact_id); + + // Adding and removing Bob + // moves him to the top of past member list. + SystemTime::shift(Duration::from_secs(5)); + add_contact_to_chat(t, chat_id, bob_contact_id).await?; + remove_contact_from_chat(t, chat_id, bob_contact_id).await?; + + let past_contacts = get_past_chat_contacts(t, chat_id).await?; + assert_eq!(past_contacts.len(), 3); + assert_eq!(past_contacts[0], bob_contact_id); + assert_eq!(past_contacts[1], fiona_contact_id); + assert_eq!(past_contacts[2], charlie_contact_id); + + Ok(()) +} + /// Test the case when Alice restores a backup older than 60 days /// with outdated member list. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -3428,13 +3838,11 @@ async fn test_restore_backup_after_60_days() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; let fiona = &tcm.fiona().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?; - - let charlie_addr = "charlie@example.com"; - let alice_charlie_contact_id = Contact::create(alice, "Charlie", charlie_addr).await?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?; @@ -3456,7 +3864,6 @@ async fn test_restore_backup_after_60_days() -> Result<()> { assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1); let remove_message = alice.pop_sent_msg().await; - assert_eq!(remove_message.payload.contains(charlie_addr), true); bob.recv_msg(&remove_message).await; // 60 days pass. @@ -3465,8 +3872,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> { assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0); // Bob adds Fiona to the chat. - let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap(); - let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?; + let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await; add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?; let add_message = bob.pop_sent_msg().await; @@ -3517,8 +3923,10 @@ async fn test_restore_backup_after_60_days() -> Result<()> { alice.recv_msg(&bob_sent_text).await; fiona.recv_msg(&bob_sent_text).await; + // Alice did not knew that Charlie is not part of the group + // when sending a message, so sent it to Charlie. bob.recv_msg(&alice_sent_text).await; - fiona.recv_msg(&alice_sent_text).await; + charlie.recv_msg(&alice_sent_text).await; // Alice should have learned about Charlie not being part of the group // by receiving Bob's message. @@ -3530,7 +3938,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> { // Charlie is not part of the chat. assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3); assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0); - let bob_charlie_contact_id = Contact::create(bob, "Charlie", charlie_addr).await?; + let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await; assert!(!is_contact_in_chat(bob, bob_chat_id, bob_charlie_contact_id).await?); assert_eq!(get_chat_contacts(fiona, fiona_chat_id).await?.len(), 3); @@ -3547,3 +3955,228 @@ async fn test_one_to_one_chat_no_group_member_timestamps() { let payload = sent.payload; assert!(!payload.contains("Chat-Group-Member-Timestamps:")); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_edit_request() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_chat(bob).await; + + // Alice sends a message with typos, followed by a correction message + let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await; + let alice_msg = sent1.load_from_db().await; + assert_eq!(alice_msg.text, "zext me in delra.cat"); + + send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?; + let sent2 = alice.pop_sent_msg().await; + let test = Message::load_from_db(alice, alice_msg.id).await?; + assert_eq!(test.text, "Text me on Delta.Chat"); + + // Bob receives both messages and has the correct text at the end + let bob_msg = bob.recv_msg(&sent1).await; + assert_eq!(bob_msg.text, "zext me in delra.cat"); + + bob.recv_msg_opt(&sent2).await; + let test = Message::load_from_db(bob, bob_msg.id).await?; + assert_eq!(test.text, "Text me on Delta.Chat"); + assert!(test.is_edited()); + + // alice has another device, and sees the correction also there + let alice2 = tcm.alice().await; + let alice2_msg = alice2.recv_msg(&sent1).await; + assert_eq!(alice2_msg.text, "zext me in delra.cat"); + + alice2.recv_msg_opt(&sent2).await; + let test = Message::load_from_db(&alice2, alice2_msg.id).await?; + assert_eq!(test.text, "Text me on Delta.Chat"); + assert!(test.is_edited()); + + // Alice forwards the edited message, the new message shouldn't have the "edited" mark. + forward_msgs(&alice2, &[test.id], test.chat_id).await?; + let forwarded = alice2.get_last_msg().await; + assert!(!forwarded.is_edited()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_receive_edit_request_after_removal() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_chat(bob).await; + + // Alice sends a messag with typos, followed by a correction message + let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await; + let alice_msg = sent1.load_from_db().await; + send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?; + let sent2 = alice.pop_sent_msg().await; + + // Bob receives first message, deletes it and then ignores the correction + let bob_msg = bob.recv_msg(&sent1).await; + let bob_chat_id = bob_msg.chat_id; + assert_eq!(bob_msg.text, "zext me in delra.cat"); + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1); + + delete_msgs(bob, &[bob_msg.id]).await?; + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0); + + bob.recv_msg_trash(&sent2).await; + assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_cannot_send_edit_request() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let chat_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob]) + .await; + + // Alice can edit her message + let sent1 = alice.send_text(chat_id, "foo").await; + send_edit_request(alice, sent1.sender_msg_id, "bar".to_string()).await?; + + // Bob cannot edit Alice's message + let msg = bob.recv_msg(&sent1).await; + assert!(send_edit_request(bob, msg.id, "bar".to_string()) + .await + .is_err()); + + // HTML messages cannot be edited + let mut msg = Message::new_text("plain text".to_string()); + msg.set_html(Some("html text".to_string())); + let sent2 = alice.send_msg(chat_id, &mut msg).await; + assert!(msg.has_html()); + assert!( + send_edit_request(alice, sent2.sender_msg_id, "foo".to_string()) + .await + .is_err() + ); + + // Info messages cannot be edited + set_chat_name(alice, chat_id, "bar").await?; + let msg = alice.get_last_msg().await; + assert!(msg.is_info()); + assert_eq!(msg.from_id, ContactId::SELF); + assert!(send_edit_request(alice, msg.id, "bar".to_string()) + .await + .is_err()); + + // Videochat invitations cannot be edited + alice + .set_config(Config::WebrtcInstance, Some("https://foo.bar")) + .await?; + let msg_id = send_videochat_invitation(alice, chat_id).await?; + assert!(send_edit_request(alice, msg_id, "bar".to_string()) + .await + .is_err()); + + // If not text was given initally, there is nothing to edit + // (this also avoids complexity in UI element changes; focus is typos and rewordings) + let mut msg = Message::new(Viewtype::File); + msg.make_vcard(alice, &[ContactId::SELF]).await?; + let sent3 = alice.send_msg(chat_id, &mut msg).await; + assert!(msg.text.is_empty()); + assert!( + send_edit_request(alice, sent3.sender_msg_id, "bar".to_string()) + .await + .is_err() + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_delete_request() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_chat(bob).await; + let bob_chat = bob.create_chat(alice).await; + + // Bobs sends a message to Alice, so Alice learns Bob's key + let sent0 = bob.send_text(bob_chat.id, "¡ola!").await; + alice.recv_msg(&sent0).await; + + // Alice sends a message, then sends a deletion request + let sent1 = alice.send_text(alice_chat.id, "wtf").await; + let alice_msg = sent1.load_from_db().await; + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2); + + message::delete_msgs_ex(alice, &[alice_msg.id], true).await?; + let sent2 = alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1); + + // Bob receives both messages and has nothing the end + let bob_msg = bob.recv_msg(&sent1).await; + assert_eq!(bob_msg.text, "wtf"); + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2); + + bob.recv_msg_opt(&sent2).await; + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1); + + // Alice has another device, and there is also nothing at the end + let alice2 = &tcm.alice().await; + alice2.recv_msg(&sent0).await; + let alice2_msg = alice2.recv_msg(&sent1).await; + assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2); + + alice2.recv_msg_opt(&sent2).await; + assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_delete_request_no_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_email_chat(bob).await; + + // Alice sends a message, then tries to send a deletion request which fails. + let sent1 = alice.send_text(alice_chat.id, "wtf").await; + assert!(message::delete_msgs_ex(alice, &[sent1.sender_msg_id], true) + .await + .is_err()); + sent1.load_from_db().await; + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1); + Ok(()) +} + +/// Tests that in multi-device setup +/// second device learns the key of a contact +/// via Autocrypt-Gossip in 1:1 chats. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_oneone_gossip() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + + tcm.section("Alice imports Bob's vCard and sends a message from the first device"); + let alice_chat = alice.create_chat(bob).await; + let sent_msg = alice.send_text(alice_chat.id, "Hello Bob!").await; + + tcm.section("Alice receives a copy on second device"); + let rcvd_msg = alice2.recv_msg(&sent_msg).await; + assert_eq!(rcvd_msg.get_showpadlock(), true); + + tcm.section("Alice sends a message from the second device"); + let alice2_chat_id = rcvd_msg.chat_id; + let sent_msg2 = alice2 + .send_text(alice2_chat_id, "Hello from second device!") + .await; + + tcm.section("Bob receives a message from the second device"); + let rcvd_msg2 = bob.recv_msg(&sent_msg2).await; + assert_eq!(rcvd_msg2.get_showpadlock(), true); + assert_eq!(rcvd_msg2.text, "Hello from second device!"); + + Ok(()) +} diff --git a/src/chatlist.rs b/src/chatlist.rs index 85a6067fa6..d8f7043406 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,7 +1,7 @@ //! # Chat list module. use anyhow::{ensure, Context as _, Result}; -use once_cell::sync::Lazy; +use std::sync::LazyLock; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; use crate::constants::{ @@ -17,8 +17,8 @@ use crate::summary::Summary; use crate::tools::IsNoneOrEmpty; /// Regex to find out if a query should filter by unread messages. -pub static IS_UNREAD_FILTER: Lazy = - Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap()); +pub static IS_UNREAD_FILTER: LazyLock = + LazyLock::new(|| regex::Regex::new(r"\bis:unread\b").unwrap()); /// An object representing a single chatlist in memory. /// @@ -322,7 +322,7 @@ impl Chatlist { (chat_id, MessageState::OutDraft), ) .await - .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?; + .with_context(|| format!("failed to get msg ID for chat {chat_id}"))?; ids.push((chat_id, msg_id)); } Ok(Chatlist { ids }) @@ -407,16 +407,17 @@ impl Chatlist { let lastcontact = if let Some(lastmsg) = &lastmsg { if lastmsg.from_id == ContactId::SELF { None + } else if chat.typ == Chattype::Group + || chat.typ == Chattype::Broadcast + || chat.typ == Chattype::Mailinglist + || chat.is_self_talk() + { + let lastcontact = Contact::get_by_id(context, lastmsg.from_id) + .await + .context("loading contact failed")?; + Some(lastcontact) } else { - match chat.typ { - Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => { - let lastcontact = Contact::get_by_id(context, lastmsg.from_id) - .await - .context("loading contact failed")?; - Some(lastcontact) - } - Chattype::Single => None, - } + None } } else { None @@ -479,6 +480,7 @@ pub async fn get_last_message_for_chat( #[cfg(test)] mod tests { use super::*; + use crate::chat::save_msgs; use crate::chat::{ add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat, send_text_msg, ProtectionStatus, @@ -486,22 +488,24 @@ mod tests { use crate::receive_imf::receive_imf; use crate::stock_str::StockMessage; use crate::test_utils::TestContext; + use crate::test_utils::TestContextManager; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_try_load() { - let t = TestContext::new_bob().await; - let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat") + let mut tcm = TestContextManager::new(); + let bob = &tcm.bob().await; + let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat") .await .unwrap(); - let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat") + let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat") .await .unwrap(); - let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat") + let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat") .await .unwrap(); // check that the chatlist starts with the most recent message - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap(); assert_eq!(chats.len(), 3); assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3); assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2); @@ -517,51 +521,49 @@ mod tests { // 2s here. for chat_id in &[chat_id1, chat_id3, chat_id2] { let mut msg = Message::new_text("hello".to_string()); - chat_id.set_draft(&t, Some(&mut msg)).await.unwrap(); + chat_id.set_draft(bob, Some(&mut msg)).await.unwrap(); } - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap(); assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2); // check chatlist query and archive functionality - let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap(); + let chats = Chatlist::try_load(bob, 0, Some("b"), None).await.unwrap(); assert_eq!(chats.len(), 1); // receive a message from alice - let alice = TestContext::new_alice().await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat") + let alice = &tcm.alice().await; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat") .await .unwrap(); add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "bob", "bob@example.net") - .await - .unwrap(), + alice.add_or_lookup_contact_id(bob).await, ) .await .unwrap(); - send_text_msg(&alice, alice_chat_id, "hi".into()) + send_text_msg(alice, alice_chat_id, "hi".into()) .await .unwrap(); let sent_msg = alice.pop_sent_msg().await; - t.recv_msg(&sent_msg).await; - let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None) + bob.recv_msg(&sent_msg).await; + let chats = Chatlist::try_load(bob, 0, Some("is:unread"), None) .await .unwrap(); assert_eq!(chats.len(), 1); - let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None) + let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None) .await .unwrap(); assert_eq!(chats.len(), 0); chat_id1 - .set_visibility(&t, ChatVisibility::Archived) + .set_visibility(bob, ChatVisibility::Archived) .await .ok(); - let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None) + let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None) .await .unwrap(); assert_eq!(chats.len(), 1); @@ -787,6 +789,31 @@ mod tests { assert!(summary_res.is_ok()); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_get_summary_for_saved_messages() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat_alice = alice.create_chat(&bob).await; + + send_text_msg(&alice, chat_alice.id, "hi".into()).await?; + let sent1 = alice.pop_sent_msg().await; + save_msgs(&alice, &[sent1.sender_msg_id]).await?; + let chatlist = Chatlist::try_load(&alice, 0, None, None).await?; + let summary = chatlist.get_summary(&alice, 0, None).await?; + assert_eq!(summary.prefix.unwrap().to_string(), "Me"); + assert_eq!(summary.text, "hi"); + + let msg = bob.recv_msg(&sent1).await; + save_msgs(&bob, &[msg.id]).await?; + let chatlist = Chatlist::try_load(&bob, 0, None, None).await?; + let summary = chatlist.get_summary(&bob, 0, None).await?; + assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org"); + assert_eq!(summary.text, "hi"); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_load_broken() { let t = TestContext::new_bob().await; diff --git a/src/config.rs b/src/config.rs index 8ac186be75..1fa11d49ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use std::env; use std::path::Path; use std::str::FromStr; -use anyhow::{ensure, Context as _, Result}; +use anyhow::{bail, ensure, Context as _, Result}; use base64::Engine as _; use deltachat_contact_tools::{addr_cmp, sanitize_single_line}; use serde::{Deserialize, Serialize}; @@ -13,10 +13,12 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; use tokio::fs; use crate::blob::BlobObject; +use crate::configure::EnteredLoginParam; use crate::constants; use crate::context::Context; use crate::events::EventType; use crate::log::LogExt; +use crate::login_param::ConfiguredLoginParam; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::provider::{get_provider_by_id, Provider}; use crate::sync::{self, Sync::*, SyncData}; @@ -182,21 +184,11 @@ pub enum Config { #[strum(props(default = "0"))] // also change MediaQuality.default() on changes MediaQuality, - /// If set to "1", on the first time `start_io()` is called after configuring, - /// the newest existing messages are fetched. - /// Existing recipients are added to the contact database regardless of this setting. - #[strum(props(default = "0"))] - FetchExistingMsgs, - /// If set to "1", then existing messages are considered to be already fetched. /// This flag is reset after successful configuration. #[strum(props(default = "1"))] FetchedExistingMsgs, - /// Type of the OpenPGP key to generate. - #[strum(props(default = "0"))] - KeyGenType, - /// Timer in seconds after which the message is deleted from the /// server. /// @@ -220,9 +212,6 @@ pub enum Config { /// `ProviderOptions::delete_to_trash`. DeleteToTrash, - /// Save raw MIME messages with headers in the database if true. - SaveMimeHeaders, - /// The primary email address. Also see `SecondaryAddrs`. ConfiguredAddr, @@ -455,6 +444,13 @@ pub enum Config { /// If it has not changed, we do not store /// the device token again. DeviceToken, + + /// Device token encrypted with OpenPGP. + /// + /// We store encrypted token next to `device_token` + /// to avoid encrypting it differently and + /// storing the same token multiple times on the server. + EncryptedDeviceToken, } impl Config { @@ -481,7 +477,10 @@ impl Config { /// Whether the config option needs an IO scheduler restart to take effect. pub(crate) fn needs_io_restart(&self) -> bool { - matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch) + matches!( + self, + Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch + ) } } @@ -528,21 +527,22 @@ impl Context { // Default values let val = match key { Config::BccSelf => match Box::pin(self.is_chatmail()).await? { - false => Some("1"), - true => Some("0"), + false => Some("1".to_string()), + true => Some("0".to_string()), }, - Config::ConfiguredInboxFolder => Some("INBOX"), + Config::ConfiguredInboxFolder => Some("INBOX".to_string()), Config::DeleteServerAfter => { match !Box::pin(self.get_config_bool(Config::BccSelf)).await? && Box::pin(self.is_chatmail()).await? { - true => Some("1"), - false => Some("0"), + true => Some("1".to_string()), + false => Some("0".to_string()), } } - _ => key.get_str("default"), + Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?, + _ => key.get_str("default").map(|s| s.to_string()), }; - Ok(val.map(|s| s.to_string())) + Ok(val) } /// Returns Some(T) if a value for the given key is set and was successfully parsed. @@ -707,9 +707,7 @@ impl Context { | Config::SentboxWatch | Config::MvboxMove | Config::OnlyFetchMvbox - | Config::FetchExistingMsgs | Config::DeleteToTrash - | Config::SaveMimeHeaders | Config::Configured | Config::Bot | Config::NotifyAboutWrongPw @@ -810,6 +808,19 @@ impl Context { .set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None) .await?; } + Config::ConfiguredAddr => { + if self.is_configured().await? { + bail!("Cannot change ConfiguredAddr"); + } + if let Some(addr) = value { + info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"); + ConfiguredLoginParam::from_json(&format!( + r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"# + ))? + .save_to_transports_table(self, &EnteredLoginParam::default()) + .await?; + } + } _ => { self.sql.set_raw_config(key.as_ref(), value).await?; } @@ -896,6 +907,7 @@ impl Context { /// primary address (if exists) as a secondary address. /// /// This should only be used by test code and during configure. + #[cfg(test)] // AEAP is disabled, but there are still tests for it pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> { self.quota.write().await.take(); @@ -909,7 +921,8 @@ impl Context { ) .await?; - self.set_config_internal(Config::ConfiguredAddr, Some(primary_new)) + self.sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new)) .await?; self.emit_event(EventType::ConnectivityChanged); Ok(()) @@ -956,397 +969,4 @@ fn get_config_keys_string() -> String { } #[cfg(test)] -mod tests { - use num_traits::FromPrimitive; - - use super::*; - use crate::test_utils::{sync, TestContext, TestContextManager}; - - #[test] - fn test_to_string() { - assert_eq!(Config::MailServer.to_string(), "mail_server"); - assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer)); - - assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys"); - assert_eq!( - Config::from_str("sys.config_keys"), - Ok(Config::SysConfigKeys) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_addr() { - let t = TestContext::new().await; - - // Test that uppercase address get lowercased. - assert!(t - .set_config(Config::Addr, Some("Foobar@eXample.oRg")) - .await - .is_ok()); - assert_eq!( - t.get_config(Config::Addr).await.unwrap().unwrap(), - "foobar@example.org" - ); - } - - /// Tests that "bot" config can only be set to "0" or "1". - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_bot() { - let t = TestContext::new().await; - - assert!(t.set_config(Config::Bot, None).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("0")).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("1")).await.is_ok()); - assert!(t.set_config(Config::Bot, Some("2")).await.is_err()); - assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_media_quality_config_option() { - let t = TestContext::new().await; - let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); - assert_eq!(media_quality, 0); - let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); - assert_eq!(media_quality, constants::MediaQuality::Balanced); - - t.set_config(Config::MediaQuality, Some("1")).await.unwrap(); - - let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); - assert_eq!(media_quality, 1); - assert_eq!(constants::MediaQuality::Worse as i32, 1); - let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); - assert_eq!(media_quality, constants::MediaQuality::Worse); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ui_config() -> Result<()> { - let t = TestContext::new().await; - - assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None); - - t.set_ui_config("ui.android.screen_security", Some("safe")) - .await?; - assert_eq!( - t.get_ui_config("ui.android.screen_security").await?, - Some("safe".to_string()) - ); - - t.set_ui_config("ui.android.screen_security", None).await?; - assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None); - - assert!(t.set_ui_config("configured", Some("bar")).await.is_err()); - - Ok(()) - } - - /// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012 - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_config_bool() -> Result<()> { - let t = TestContext::new().await; - - // We need some config that defaults to true - let c = Config::E2eeEnabled; - assert_eq!(t.get_config_bool(c).await?, true); - t.set_config_bool(c, false).await?; - assert_eq!(t.get_config_bool(c).await?, false); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_self_addrs() -> Result<()> { - let alice = TestContext::new_alice().await; - - assert!(alice.is_self_addr("alice@example.org").await?); - assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]); - assert!(!alice.is_self_addr("alice@alice.com").await?); - - // Test adding the same primary address - alice.set_primary_self_addr("alice@example.org").await?; - alice.set_primary_self_addr("Alice@Example.Org").await?; - assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]); - - // Test adding a new (primary) self address - // The address is trimmed during configure by `LoginParam::from_database()`, - // so `set_primary_self_addr()` doesn't have to trim it. - alice.set_primary_self_addr("Alice@alice.com").await?; - assert!(alice.is_self_addr("aliCe@example.org").await?); - assert!(alice.is_self_addr("alice@alice.com").await?); - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["Alice@alice.com", "Alice@Example.Org"] - ); - - // Check that the entry is not duplicated - alice.set_primary_self_addr("alice@alice.com").await?; - alice.set_primary_self_addr("alice@alice.com").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@alice.com", "Alice@Example.Org"] - ); - - // Test switching back - alice.set_primary_self_addr("alice@example.org").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@example.org", "alice@alice.com"] - ); - - // Test setting a new primary self address, the previous self address - // should be kept as a secondary self address - alice.set_primary_self_addr("alice@alice.xyz").await?; - assert_eq!( - alice.get_all_self_addrs().await?, - vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"] - ); - assert!(alice.is_self_addr("alice@example.org").await?); - assert!(alice.is_self_addr("alice@alice.com").await?); - assert!(alice.is_self_addr("Alice@alice.xyz").await?); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mdns_default_behaviour() -> Result<()> { - let t = &TestContext::new_alice().await; - assert!(t.should_request_mdns().await?); - assert!(t.should_send_mdns().await?); - assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); - // The setting should be displayed correctly. - assert!(t.get_config_bool(Config::MdnsEnabled).await?); - - t.set_config_bool(Config::Bot, true).await?; - assert!(!t.should_request_mdns().await?); - assert!(t.should_send_mdns().await?); - assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); - assert!(t.get_config_bool(Config::MdnsEnabled).await?); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_server_after_default() -> Result<()> { - let t = &TestContext::new_alice().await; - - // Check that the settings are displayed correctly. - assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string())); - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - - // Leaving emails on the server even w/o `BccSelf` is a good default at least because other - // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail - // does). - t.set_config_bool(Config::BccSelf, false).await?; - assert_eq!( - t.get_config(Config::DeleteServerAfter).await?, - Some("0".to_string()) - ); - Ok(()) - } - - const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync() -> Result<()> { - let alice0 = TestContext::new_alice().await; - let alice1 = TestContext::new_alice().await; - for a in [&alice0, &alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - - let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?; - // Alice1 has a different config value. - alice1 - .set_config_bool(Config::MdnsEnabled, !mdns_enabled) - .await?; - // This changes nothing, but still sends a sync message. - alice0 - .set_config_bool(Config::MdnsEnabled, mdns_enabled) - .await?; - sync(&alice0, &alice1).await; - assert_eq!( - alice1.get_config_bool(Config::MdnsEnabled).await?, - mdns_enabled - ); - - // Reset to default. Test that it's not synced because defaults may differ across client - // versions. - alice0.set_config(Config::MdnsEnabled, None).await?; - alice0.set_config_bool(Config::MdnsEnabled, false).await?; - sync(&alice0, &alice1).await; - assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); - - for key in [Config::ShowEmails, Config::MvboxMove] { - let val = alice0.get_config_bool(key).await?; - alice0.set_config_bool(key, !val).await?; - sync(&alice0, &alice1).await; - assert_eq!(alice1.get_config_bool(key).await?, !val); - } - - // `Config::SyncMsgs` mustn't be synced. - alice0.set_config_bool(Config::SyncMsgs, false).await?; - alice0.set_config_bool(Config::SyncMsgs, true).await?; - alice0.set_config_bool(Config::MdnsEnabled, true).await?; - sync(&alice0, &alice1).await; - 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())) - .await?; - sync(&alice0, &alice1).await; - // There was a bug that a sync message creates the self-chat with the user avatar instead of - // the special icon and that remains so when the self-chat becomes user-visible. Let's check - // this. - let self_chat = alice0.get_self_chat().await; - let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap(); - assert_eq!( - self_chat_avatar_path, - alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE) - ); - 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.send_sync_msg().await?; - alice0.pop_sent_sync_msg().await; - 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()) - ); - 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.send_sync_msg().await?; - alice0.pop_sent_sync_msg().await; - 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(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_event_config_synced() -> Result<()> { - let alice0 = TestContext::new_alice().await; - let alice1 = TestContext::new_alice().await; - for a in [&alice0, &alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - - alice0 - .set_config(Config::Displayname, Some("Alice Sync")) - .await?; - alice0 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - sync(&alice0, &alice1).await; - assert_eq!( - alice1.get_config(Config::Displayname).await?, - Some("Alice Sync".to_string()) - ); - alice1 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - - alice0.set_config(Config::Displayname, None).await?; - alice0 - .evtracker - .get_matching(|e| { - matches!( - e, - EventType::ConfigSynced { - key: Config::Displayname - } - ) - }) - .await; - - Ok(()) - } -} +mod config_tests; diff --git a/src/config/config_tests.rs b/src/config/config_tests.rs new file mode 100644 index 0000000000..c35e9a0230 --- /dev/null +++ b/src/config/config_tests.rs @@ -0,0 +1,392 @@ +use num_traits::FromPrimitive; + +use super::*; +use crate::test_utils::{sync, TestContext, TestContextManager}; + +#[test] +fn test_to_string() { + assert_eq!(Config::MailServer.to_string(), "mail_server"); + assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer)); + + assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys"); + assert_eq!( + Config::from_str("sys.config_keys"), + Ok(Config::SysConfigKeys) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_addr() { + let t = TestContext::new().await; + + // Test that uppercase address get lowercased. + assert!(t + .set_config(Config::Addr, Some("Foobar@eXample.oRg")) + .await + .is_ok()); + assert_eq!( + t.get_config(Config::Addr).await.unwrap().unwrap(), + "foobar@example.org" + ); +} + +/// Tests that "bot" config can only be set to "0" or "1". +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_bot() { + let t = TestContext::new().await; + + assert!(t.set_config(Config::Bot, None).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("0")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("1")).await.is_ok()); + assert!(t.set_config(Config::Bot, Some("2")).await.is_err()); + assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_media_quality_config_option() { + let t = TestContext::new().await; + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); + assert_eq!(media_quality, 0); + let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); + assert_eq!(media_quality, constants::MediaQuality::Balanced); + + t.set_config(Config::MediaQuality, Some("1")).await.unwrap(); + + let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap(); + assert_eq!(media_quality, 1); + assert_eq!(constants::MediaQuality::Worse as i32, 1); + let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); + assert_eq!(media_quality, constants::MediaQuality::Worse); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ui_config() -> Result<()> { + let t = TestContext::new().await; + + assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None); + + t.set_ui_config("ui.android.screen_security", Some("safe")) + .await?; + assert_eq!( + t.get_ui_config("ui.android.screen_security").await?, + Some("safe".to_string()) + ); + + t.set_ui_config("ui.android.screen_security", None).await?; + assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None); + + assert!(t.set_ui_config("configured", Some("bar")).await.is_err()); + + Ok(()) +} + +/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012 +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_config_bool() -> Result<()> { + let t = TestContext::new().await; + + // We need some config that defaults to true + let c = Config::MdnsEnabled; + assert_eq!(t.get_config_bool(c).await?, true); + t.set_config_bool(c, false).await?; + assert_eq!(t.get_config_bool(c).await?, false); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_addrs() -> Result<()> { + let alice = TestContext::new_alice().await; + + assert!(alice.is_self_addr("alice@example.org").await?); + assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]); + assert!(!alice.is_self_addr("alice@alice.com").await?); + + // Test adding the same primary address + alice.set_primary_self_addr("alice@example.org").await?; + alice.set_primary_self_addr("Alice@Example.Org").await?; + assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]); + + // Test adding a new (primary) self address + // The address is trimmed during configure by `LoginParam::from_database()`, + // so `set_primary_self_addr()` doesn't have to trim it. + alice.set_primary_self_addr("Alice@alice.com").await?; + assert!(alice.is_self_addr("aliCe@example.org").await?); + assert!(alice.is_self_addr("alice@alice.com").await?); + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["Alice@alice.com", "Alice@Example.Org"] + ); + + // Check that the entry is not duplicated + alice.set_primary_self_addr("alice@alice.com").await?; + alice.set_primary_self_addr("alice@alice.com").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@alice.com", "Alice@Example.Org"] + ); + + // Test switching back + alice.set_primary_self_addr("alice@example.org").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@example.org", "alice@alice.com"] + ); + + // Test setting a new primary self address, the previous self address + // should be kept as a secondary self address + alice.set_primary_self_addr("alice@alice.xyz").await?; + assert_eq!( + alice.get_all_self_addrs().await?, + vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"] + ); + assert!(alice.is_self_addr("alice@example.org").await?); + assert!(alice.is_self_addr("alice@alice.com").await?); + assert!(alice.is_self_addr("Alice@alice.xyz").await?); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdns_default_behaviour() -> Result<()> { + let t = &TestContext::new_alice().await; + assert!(t.should_request_mdns().await?); + assert!(t.should_send_mdns().await?); + assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); + // The setting should be displayed correctly. + assert!(t.get_config_bool(Config::MdnsEnabled).await?); + + t.set_config_bool(Config::Bot, true).await?; + assert!(!t.should_request_mdns().await?); + assert!(t.should_send_mdns().await?); + assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none()); + assert!(t.get_config_bool(Config::MdnsEnabled).await?); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_server_after_default() -> Result<()> { + let t = &TestContext::new_alice().await; + + // Check that the settings are displayed correctly. + assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string())); + assert_eq!( + t.get_config(Config::DeleteServerAfter).await?, + Some("0".to_string()) + ); + + // Leaving emails on the server even w/o `BccSelf` is a good default at least because other + // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail + // does). + t.set_config_bool(Config::BccSelf, false).await?; + assert_eq!( + t.get_config(Config::DeleteServerAfter).await?, + Some("0".to_string()) + ); + Ok(()) +} + +const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync() -> Result<()> { + let alice0 = TestContext::new_alice().await; + let alice1 = TestContext::new_alice().await; + for a in [&alice0, &alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + + let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?; + // Alice1 has a different config value. + alice1 + .set_config_bool(Config::MdnsEnabled, !mdns_enabled) + .await?; + // This changes nothing, but still sends a sync message. + alice0 + .set_config_bool(Config::MdnsEnabled, mdns_enabled) + .await?; + sync(&alice0, &alice1).await; + assert_eq!( + alice1.get_config_bool(Config::MdnsEnabled).await?, + mdns_enabled + ); + + // Reset to default. Test that it's not synced because defaults may differ across client + // versions. + alice0.set_config(Config::MdnsEnabled, None).await?; + alice0.set_config_bool(Config::MdnsEnabled, false).await?; + sync(&alice0, &alice1).await; + assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); + + for key in [Config::ShowEmails, Config::MvboxMove] { + let val = alice0.get_config_bool(key).await?; + alice0.set_config_bool(key, !val).await?; + sync(&alice0, &alice1).await; + assert_eq!(alice1.get_config_bool(key).await?, !val); + } + + // `Config::SyncMsgs` mustn't be synced. + alice0.set_config_bool(Config::SyncMsgs, false).await?; + alice0.set_config_bool(Config::SyncMsgs, true).await?; + alice0.set_config_bool(Config::MdnsEnabled, true).await?; + sync(&alice0, &alice1).await; + 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())) + .await?; + sync(&alice0, &alice1).await; + // There was a bug that a sync message creates the self-chat with the user avatar instead of + // the special icon and that remains so when the self-chat becomes user-visible. Let's check + // this. + let self_chat = alice0.get_self_chat().await; + let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap(); + assert_eq!( + self_chat_avatar_path, + alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE) + ); + 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.send_sync_msg().await?; + alice0.pop_sent_sync_msg().await; + 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()) + ); + 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.send_sync_msg().await?; + alice0.pop_sent_sync_msg().await; + 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(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_event_config_synced() -> Result<()> { + let alice0 = TestContext::new_alice().await; + let alice1 = TestContext::new_alice().await; + for a in [&alice0, &alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + + alice0 + .set_config(Config::Displayname, Some("Alice Sync")) + .await?; + alice0 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + sync(&alice0, &alice1).await; + assert_eq!( + alice1.get_config(Config::Displayname).await?, + Some("Alice Sync".to_string()) + ); + alice1 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + + alice0.set_config(Config::Displayname, None).await?; + alice0 + .evtracker + .get_matching(|e| { + matches!( + e, + EventType::ConfigSynced { + key: Config::Displayname + } + ) + }) + .await; + + Ok(()) +} diff --git a/src/configure.rs b/src/configure.rs index 1fd561fbc0..f5b957cd4b 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -16,7 +16,7 @@ pub(crate) mod server_params; use anyhow::{bail, ensure, format_err, Context as _, Result}; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; -use deltachat_contact_tools::EmailAddress; +use deltachat_contact_tools::{addr_normalize, EmailAddress}; use futures::FutureExt; use futures_lite::FutureExt as _; use percent_encoding::utf8_percent_encode; @@ -28,17 +28,18 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT; use crate::context::Context; use crate::imap::Imap; use crate::log::LogExt; +pub use crate::login_param::EnteredLoginParam; use crate::login_param::{ ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, - ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam, + ConnectionCandidate, EnteredCertificateChecks, ProxyConfig, }; use crate::message::Message; use crate::oauth2::get_oauth2_addr; -use crate::provider::{Protocol, Socket, UsernamePattern}; +use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; use crate::smtp::Smtp; use crate::sync::Sync::*; use crate::tools::time; -use crate::{chat, e2ee, provider}; +use crate::{chat, provider}; use crate::{stock_str, EventType}; use deltachat_contact_tools::addr_cmp; @@ -61,11 +62,62 @@ macro_rules! progress { impl Context { /// Checks if the context is already configured. pub async fn is_configured(&self) -> Result { - self.sql.get_raw_config_bool("configured").await + self.sql.exists("SELECT COUNT(*) FROM transports", ()).await } - /// Configures this account with the currently set parameters. + /// Configures this account with the currently provided parameters. + /// + /// Deprecated since 2025-02; use `add_transport_from_qr()` + /// or `add_or_update_transport()` instead. pub async fn configure(&self) -> Result<()> { + let mut param = EnteredLoginParam::load(self).await?; + + self.add_transport_inner(&mut param).await + } + + /// Configures a new email account using the provided parameters + /// and adds it as a transport. + /// + /// If the email address is the same as an existing transport, + /// then this existing account will be reconfigured instead of a new one being added. + /// + /// This function stops and starts IO as needed. + /// + /// Usually it will be enough to only set `addr` and `imap.password`, + /// and all the other settings will be autoconfigured. + /// + /// During configuration, ConfigureProgress events are emitted; + /// they indicate a successful configuration as well as errors + /// and may be used to create a progress bar. + /// This function will return after configuration is finished. + /// + /// If configuration is successful, + /// the working server parameters will be saved + /// and used for connecting to the server. + /// The parameters entered by the user will be saved separately + /// so that they can be prefilled when the user opens the server-configuration screen again. + /// + /// See also: + /// - [Self::is_configured()] to check whether there is + /// at least one working transport. + /// - [Self::add_transport_from_qr()] to add a transport + /// from a server encoded in a QR code. + /// - [Self::list_transports()] to get a list of all configured transports. + /// - [Self::delete_transport()] to remove a transport. + pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> { + self.stop_io().await; + let result = self.add_transport_inner(param).await; + if result.is_err() { + if let Ok(true) = self.is_configured().await { + self.start_io().await; + } + return result; + } + self.start_io().await; + Ok(()) + } + + async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> { ensure!( !self.scheduler.is_running().await, "cannot configure, already running" @@ -74,55 +126,116 @@ impl Context { self.sql.is_open().await, "cannot configure, database not opened." ); + param.addr = addr_normalize(¶m.addr); + let old_addr = self.get_config(Config::ConfiguredAddr).await?; + if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) { + let error_msg = "Changing your email address is not supported right now. Check back in a few months!"; + progress!(self, 0, Some(error_msg.to_string())); + bail!(error_msg); + } let cancel_channel = self.alloc_ongoing().await?; let res = self - .inner_configure() + .inner_configure(param) .race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled")))) .await; self.free_ongoing().await; if let Err(err) = res.as_ref() { - progress!( - self, - 0, - Some( - stock_str::configuration_failed( - self, - // We are using Anyhow's .context() and to show the - // inner error, too, we need the {:#}: - &format!("{err:#}"), - ) - .await - ) - ); + // We are using Anyhow's .context() and to show the + // inner error, too, we need the {:#}: + let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await; + progress!(self, 0, Some(error_msg.clone())); + bail!(error_msg); } else { + param.save(self).await?; progress!(self, 1000); } res } - async fn inner_configure(&self) -> Result<()> { + /// Adds a new email account as a transport + /// using the server encoded in the QR code. + /// See [Self::add_or_update_transport]. + pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> { + self.stop_io().await; + + // This code first sets the deprecated Config::Addr, Config::MailPw, etc. + // and then calls configure(), which loads them again. + // At some point, we will remove configure() + // and then simplify the code + // to directly create an EnteredLoginParam. + let result = async move { + match crate::qr::check_qr(self, qr).await? { + crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?, + crate::qr::Qr::Login { address, options } => { + crate::qr::configure_from_login_qr(self, &address, options).await? + } + _ => bail!("QR code does not contain account"), + } + self.configure().await?; + Ok(()) + } + .await; + + if result.is_err() { + if let Ok(true) = self.is_configured().await { + self.start_io().await; + } + return result; + } + self.start_io().await; + Ok(()) + } + + /// Returns the list of all email accounts that are used as a transport in the current profile. + /// Use [Self::add_or_update_transport()] to add or change a transport + /// and [Self::delete_transport()] to delete a transport. + pub async fn list_transports(&self) -> Result> { + let transports = self + .sql + .query_map( + "SELECT entered_param FROM transports", + (), + |row| row.get::<_, String>(0), + |rows| { + rows.flatten() + .map(|s| Ok(serde_json::from_str(&s)?)) + .collect::>>() + }, + ) + .await?; + + Ok(transports) + } + + /// Removes the transport with the specified email address + /// (i.e. [EnteredLoginParam::addr]). + #[expect(clippy::unused_async)] + pub async fn delete_transport(&self, _addr: &str) -> Result<()> { + bail!("Adding and removing additional transports is not supported yet. Check back in a few months!") + } + + async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> { info!(self, "Configure ..."); - let param = EnteredLoginParam::load(self).await?; let old_addr = self.get_config(Config::ConfiguredAddr).await?; - let configured_param = configure(self, ¶m).await?; + let provider = configure(self, param).await?; self.set_config_internal(Config::NotifyAboutWrongPw, Some("1")) .await?; - on_configure_completed(self, configured_param, old_addr).await?; + on_configure_completed(self, provider, old_addr).await?; Ok(()) } } async fn on_configure_completed( context: &Context, - param: ConfiguredLoginParam, + provider: Option<&'static Provider>, old_addr: Option, ) -> Result<()> { - if let Some(provider) = param.provider { + if let Some(provider) = provider { if let Some(config_defaults) = provider.config_defaults { for def in config_defaults { if !context.config_exists(def.key).await? { @@ -185,8 +298,7 @@ async fn get_configured_param( param.smtp.password.clone() }; - let proxy_config = param.proxy_config.clone(); - let proxy_enabled = proxy_config.is_some(); + let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?; let mut addr = param.addr.clone(); if param.oauth2 { @@ -345,7 +457,6 @@ async fn get_configured_param( .collect(), smtp_user: param.smtp.user.clone(), smtp_password, - proxy_config: param.proxy_config.clone(), provider, certificate_checks: match param.certificate_checks { EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic, @@ -360,14 +471,15 @@ async fn get_configured_param( Ok(configured_login_param) } -async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result { +async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result> { progress!(ctx, 1); let ctx2 = ctx.clone(); let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await }); let configured_param = get_configured_param(ctx, param).await?; - let strict_tls = configured_param.strict_tls(); + let proxy_config = ProxyConfig::load(ctx).await?; + let strict_tls = configured_param.strict_tls(proxy_config.is_some()); progress!(ctx, 550); @@ -377,15 +489,15 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result Result Result session, - Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await), + Err(err) => bail!( + "{}", + nicer_configuration_error(ctx, format!("{err:#}")).await + ), }; progress!(ctx, 850); @@ -442,7 +557,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result Result Result = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string()); +pub static DC_VERSION_STR: LazyLock = + LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string()); /// Set of characters to percent-encode in email addresses and names. pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.'); @@ -58,25 +60,6 @@ pub enum MediaQuality { Worse = 1, } -/// Type of the key to generate. -#[derive( - Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, -)] -#[repr(u8)] -pub enum KeyGenType { - #[default] - Default = 0, - - /// 2048-bit RSA. - Rsa2048 = 1, - - /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption. - Ed25519 = 2, - - /// 4096-bit RSA. - Rsa4096 = 3, -} - /// Video chat URL type. #[derive( Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, @@ -105,7 +88,6 @@ pub const DC_GCL_NO_SPECIALS: usize = 0x02; pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04; pub const DC_GCL_FOR_FORWARDING: usize = 0x08; -pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01; pub const DC_GCL_ADD_SELF: u32 = 0x02; // unchanged user avatars are resent to the recipients every some days @@ -197,16 +179,15 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4; /// if none of these flags are set, the default is chosen pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL; -/// How many existing messages shall be fetched after configuration. -pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100; - // max. weight of images to send w/o recoding pub const BALANCED_IMAGE_BYTES: usize = 500_000; pub const WORSE_IMAGE_BYTES: usize = 130_000; -// max. width/height of an avatar -pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256; +// max. width/height and bytes of an avatar +pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512; +pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000; pub(crate) const WORSE_AVATAR_SIZE: u32 = 128; +pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k. // max. width/height of images scaled down because of being too huge pub const BALANCED_IMAGE_SIZE: u32 = 1280; @@ -234,6 +215,23 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60; /// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`]. pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15; +// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX. +// Newer Delta Chats will remove the prefix as needed. +pub(crate) const EDITED_PREFIX: &str = "✏️"; + +// Strings needed to render the Autocrypt Setup Message. +// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge. +pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message"; +pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \ + used to transfer your end-to-end setup between clients. + + To decrypt and use your setup, \ + open the message in an Autocrypt-compliant client \ + and enter the setup code presented on the generating device. + + If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \ + use \"Settings / Add Second Device\" instead."; + #[cfg(test)] mod tests { use num_traits::FromPrimitive; @@ -249,16 +247,6 @@ mod tests { assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap()); } - #[test] - fn test_keygentype_values() { - // values may be written to disk and must not change - assert_eq!(KeyGenType::Default, KeyGenType::default()); - assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap()); - assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap()); - assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); - assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap()); - } - #[test] fn test_showemails_values() { // values may be written to disk and must not change diff --git a/src/contact.rs b/src/contact.rs index a27b8e3057..7f923348fe 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -25,7 +25,7 @@ use crate::blob::BlobObject; use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus}; use crate::color::str_to_color; use crate::config::Config; -use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; +use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF}; use crate::context::Context; use crate::events::EventType; use crate::key::{load_self_public_key, DcKey, SignedPublicKey}; @@ -95,6 +95,50 @@ impl ContactId { self.0 } + /// Sets display name for existing contact. + /// + /// Display name may be an empty string, + /// in which case the name displayed in the UI + /// for this contact will switch to the + /// contact's authorized name. + pub async fn set_name(self, context: &Context, name: &str) -> Result<()> { + let addr = context + .sql + .transaction(|transaction| { + let is_changed = transaction.execute( + "UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1", + (name, self), + )? > 0; + if is_changed { + update_chat_names(context, transaction, self)?; + let addr = transaction.query_row( + "SELECT addr FROM contacts WHERE id=?", + (self,), + |row| { + let addr: String = row.get(0)?; + Ok(addr) + }, + )?; + Ok(Some(addr)) + } else { + Ok(None) + } + }) + .await?; + + if let Some(addr) = addr { + chat::sync( + context, + chat::SyncId::ContactAddr(addr.to_string()), + chat::SyncAction::Rename(name.to_string()), + ) + .await + .log_err(context) + .ok(); + } + Ok(()) + } + /// Mark contact as bot. pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> { context @@ -247,7 +291,16 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result + // is merged. + Ok(contact_tools::make_vcard(&vcard_contacts) + .trim_end() + .to_string()) } /// Imports contacts from the given vCard. @@ -496,7 +549,11 @@ pub enum Origin { /// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() ! SecurejoinInvited = 0x0100_0000, - /// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() ! + /// Set on Bob's side for contacts scanned from a QR code. + /// Only means the contact has been scanned from the QR code, + /// but does not mean that securejoin succeeded + /// or the key has not changed since the last scan. + /// Getting the current key verification status requires calling contact_is_verified() ! SecurejoinJoined = 0x0200_0000, /// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names @@ -830,44 +887,48 @@ impl Contact { let mut update_addr = false; - let row_id = context.sql.transaction(|transaction| { - let row = transaction.query_row( - "SELECT id, name, addr, origin, authname + let row_id = context + .sql + .transaction(|transaction| { + let row = transaction + .query_row( + "SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE", - [addr.to_string()], - |row| { - let row_id: isize = row.get(0)?; - let row_name: String = row.get(1)?; - let row_addr: String = row.get(2)?; - let row_origin: Origin = row.get(3)?; - let row_authname: String = row.get(4)?; - - Ok((row_id, row_name, row_addr, row_origin, row_authname)) - }).optional()?; - - let row_id; - if let Some((id, row_name, row_addr, row_origin, row_authname)) = row { - let update_name = manual && name != row_name; - let update_authname = !manual - && name != row_authname - && !name.is_empty() - && (origin >= row_origin - || origin == Origin::IncomingUnknownFrom - || row_authname.is_empty()); - - row_id = u32::try_from(id)?; - if origin >= row_origin && addr.as_ref() != row_addr { - update_addr = true; - } - if update_name || update_authname || update_addr || origin > row_origin { - let new_name = if update_name { - name.to_string() - } else { - row_name - }; + (addr,), + |row| { + let row_id: u32 = row.get(0)?; + let row_name: String = row.get(1)?; + let row_addr: String = row.get(2)?; + let row_origin: Origin = row.get(3)?; + let row_authname: String = row.get(4)?; - transaction - .execute( + Ok((row_id, row_name, row_addr, row_origin, row_authname)) + }, + ) + .optional()?; + + let row_id; + if let Some((id, row_name, row_addr, row_origin, row_authname)) = row { + let update_name = manual && name != row_name; + let update_authname = !manual + && name != row_authname + && !name.is_empty() + && (origin >= row_origin + || origin == Origin::IncomingUnknownFrom + || row_authname.is_empty()); + + row_id = id; + if origin >= row_origin && addr.as_ref() != row_addr { + update_addr = true; + } + if update_name || update_authname || update_addr || origin > row_origin { + let new_name = if update_name { + name.to_string() + } else { + row_name + }; + + transaction.execute( "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", ( new_name, @@ -886,88 +947,38 @@ impl Contact { } else { row_authname }, - row_id + row_id, ), )?; - if update_name || update_authname { - // Update the contact name also if it is used as a group name. - // This is one of the few duplicated data, however, getting the chat list is easier this way. - let chat_id: Option = transaction.query_row( - "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", - (Chattype::Single, isize::try_from(row_id)?), - |row| { - let chat_id: ChatId = row.get(0)?; - Ok(chat_id) - } - ).optional()?; - - if let Some(chat_id) = chat_id { + if update_name || update_authname { let contact_id = ContactId::new(row_id); - let (addr, name, authname) = - transaction.query_row( - "SELECT addr, name, authname - FROM contacts - WHERE id=?", - (contact_id,), - |row| { - let addr: String = row.get(0)?; - let name: String = row.get(1)?; - let authname: String = row.get(2)?; - Ok((addr, name, authname)) - })?; - - let chat_name = if !name.is_empty() { - name - } else if !authname.is_empty() { - authname - } else { - addr - }; - - let count = transaction.execute( - "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1", - (chat_name, chat_id))?; - - if count > 0 { - // Chat name updated - context.emit_event(EventType::ChatModified(chat_id)); - chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id); - } + update_chat_names(context, transaction, contact_id)?; } + sth_modified = Modifier::Modified; } - sth_modified = Modifier::Modified; - } - } else { - let update_name = manual; - let update_authname = !manual; + } else { + let update_name = manual; + let update_authname = !manual; - transaction - .execute( + transaction.execute( "INSERT INTO contacts (name, addr, origin, authname) VALUES (?, ?, ?, ?);", - ( - if update_name { - name.to_string() - } else { - "".to_string() - }, + ( + if update_name { &name } else { "" }, &addr, origin, - if update_authname { - name.to_string() - } else { - "".to_string() - } + if update_authname { &name } else { "" }, ), )?; - sth_modified = Modifier::Created; - row_id = u32::try_from(transaction.last_insert_rowid())?; - info!(context, "Added contact id={row_id} addr={addr}."); - } - Ok(row_id) - }).await?; + sth_modified = Modifier::Created; + row_id = u32::try_from(transaction.last_insert_rowid())?; + info!(context, "Added contact id={row_id} addr={addr}."); + } + Ok(row_id) + }) + .await?; let contact_id = ContactId::new(row_id); @@ -1030,9 +1041,8 @@ impl Contact { /// /// `listflags` is a combination of flags: /// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters - /// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned. - /// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned. - /// `query` is a string to filter the list. + /// + /// `query` is a string to filter the list. pub async fn get_all( context: &Context, listflags: u32, @@ -1045,14 +1055,13 @@ impl Contact { .collect::>(); let mut add_self = false; let mut ret = Vec::new(); - let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0; let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0; let minimal_origin = if context.get_config_bool(Config::Bot).await? { Origin::Unknown } else { Origin::IncomingReplyTo }; - if flag_verified_only || query.is_some() { + if query.is_some() { let s3str_like_cmd = format!("%{}%", query.unwrap_or("")); context .sql @@ -1063,14 +1072,12 @@ impl Contact { AND c.origin>=? \ AND c.blocked=0 \ AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \ - AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \ ORDER BY c.last_seen DESC, c.id DESC;", ( ContactId::LAST_SPECIAL, minimal_origin, &s3str_like_cmd, &s3str_like_cmd, - if flag_verified_only { 0i32 } else { 1i32 }, ), |row| { let id: ContactId = row.get(0)?; @@ -1266,9 +1273,16 @@ impl Contact { .map(|k| k.dc_fingerprint().to_string()) .unwrap_or_default(); if addr < peerstate.addr { - cat_fingerprint(&mut ret, &addr, &fingerprint_self, ""); cat_fingerprint( &mut ret, + &stock_str::self_msg(context).await, + &addr, + &fingerprint_self, + "", + ); + cat_fingerprint( + &mut ret, + contact.get_display_name(), &peerstate.addr, &fingerprint_other_verified, &fingerprint_other_unverified, @@ -1276,11 +1290,18 @@ impl Contact { } else { cat_fingerprint( &mut ret, + contact.get_display_name(), &peerstate.addr, &fingerprint_other_verified, &fingerprint_other_unverified, ); - cat_fingerprint(&mut ret, &addr, &fingerprint_self, ""); + cat_fingerprint( + &mut ret, + &stock_str::self_msg(context).await, + &addr, + &fingerprint_self, + "", + ); } Ok(ret) @@ -1381,16 +1402,13 @@ impl Contact { &self.addr } - /// Get a summary of authorized name and address. - /// - /// The returned string is either "Name (email@domain.com)" or just - /// "email@domain.com" if the name is unset. + /// Get authorized name or address. /// /// This string is suitable for sending over email /// as it does not leak the locally set name. - pub fn get_authname_n_addr(&self) -> String { + pub(crate) fn get_authname_or_addr(&self) -> String { if !self.authname.is_empty() { - format!("{} ({})", self.authname, self.addr) + (&self.authname).into() } else { (&self.addr).into() } @@ -1422,7 +1440,7 @@ impl Contact { pub async fn get_profile_image(&self, context: &Context) -> Result> { if self.id == ContactId::SELF { if let Some(p) = context.get_config(Config::Selfavatar).await? { - return Ok(Some(PathBuf::from(p))); + return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already } } else if let Some(image_rel) = self.param.get(Param::ProfileImage) { if !image_rel.is_empty() { @@ -1593,6 +1611,60 @@ impl Contact { } } +// Updates the names of the chats which use the contact name. +// +// This is one of the few duplicated data, however, getting the chat list is easier this way. +fn update_chat_names( + context: &Context, + transaction: &rusqlite::Connection, + contact_id: ContactId, +) -> Result<()> { + let chat_id: Option = transaction.query_row( + "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", + (Chattype::Single, contact_id), + |row| { + let chat_id: ChatId = row.get(0)?; + Ok(chat_id) + } + ).optional()?; + + if let Some(chat_id) = chat_id { + let (addr, name, authname) = transaction.query_row( + "SELECT addr, name, authname + FROM contacts + WHERE id=?", + (contact_id,), + |row| { + let addr: String = row.get(0)?; + let name: String = row.get(1)?; + let authname: String = row.get(2)?; + Ok((addr, name, authname)) + }, + )?; + + let chat_name = if !name.is_empty() { + name + } else if !authname.is_empty() { + authname + } else { + addr + }; + + let count = transaction.execute( + "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1", + (chat_name, chat_id), + )?; + + if count > 0 { + // Chat name updated + context.emit_event(EventType::ChatModified(chat_id)); + chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id); + } + } + + Ok(()) +} + pub(crate) async fn set_blocked( context: &Context, sync: sync::Sync, @@ -1783,12 +1855,14 @@ pub(crate) async fn update_last_seen( fn cat_fingerprint( ret: &mut String, + name: &str, addr: &str, fingerprint_verified: &str, fingerprint_unverified: &str, ) { *ret += &format!( - "\n\n{}:\n{}", + "\n\n{} ({}):\n{}", + name, addr, if !fingerprint_verified.is_empty() { fingerprint_verified @@ -1800,7 +1874,7 @@ fn cat_fingerprint( && !fingerprint_unverified.is_empty() && fingerprint_verified != fingerprint_unverified { - *ret += &format!("\n\n{addr} (alternative):\n{fingerprint_unverified}"); + *ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}"); } } diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 3691d5752a..98724cc9d1 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -763,11 +763,11 @@ async fn test_contact_get_encrinfo() -> Result<()> { "End-to-end encryption preferred. Fingerprints: -alice@example.org: +Me (alice@example.org): 2E6F A2CB 23B5 32D7 2863 4B58 64B0 8F61 A9ED 9443 -bob@example.net: +Bob (bob@example.net): CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); @@ -1050,8 +1050,9 @@ async fn test_sync_create() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_make_n_import_vcard() -> Result<()> { - let alice = &TestContext::new_alice().await; - let bob = &TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; bob.set_config(Config::Displayname, Some("Bob")).await?; let avatar_path = bob.dir.path().join("avatar.png"); let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); @@ -1207,16 +1208,16 @@ async fn test_reset_encryption() -> Result<()> { let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; - assert_eq!(msg.get_showpadlock(), false); - - let msg = tcm.send_recv(bob, alice, "Hi!").await; + let msg = tcm.send_recv_accept(bob, alice, "Hi!").await; assert_eq!(msg.get_showpadlock(), true); + + let alice_bob_chat_id = msg.chat_id; let alice_bob_contact_id = msg.from_id; alice_bob_contact_id.reset_encryption(alice).await?; - let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await; + let msg = bob.recv_msg(&sent).await; assert_eq!(msg.get_showpadlock(), false); Ok(()) @@ -1235,6 +1236,7 @@ async fn test_reset_verified_encryption() -> Result<()> { let alice_bob_chat_id = msg.chat_id; let alice_bob_contact_id = msg.from_id; + alice_bob_contact_id.reset_encryption(alice).await?; // Check that the contact is still verified after resetting encryption. @@ -1250,7 +1252,8 @@ async fn test_reset_verified_encryption() -> Result<()> { "bob@example.net sent a message from another device." ); - let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await; + let msg = bob.recv_msg(&sent).await; assert_eq!(msg.get_showpadlock(), false); Ok(()) diff --git a/src/context.rs b/src/context.rs index 60a3c60f32..5af9b2ee0c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -903,12 +903,6 @@ impl Context { } res.insert("secondary_addrs", secondary_addrs); - res.insert( - "fetch_existing_msgs", - self.get_config_int(Config::FetchExistingMsgs) - .await? - .to_string(), - ); res.insert( "fetched_existing_msgs", self.get_config_bool(Config::FetchedExistingMsgs) @@ -919,12 +913,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) @@ -944,10 +932,6 @@ impl Context { res.insert("configured_trash_folder", configured_trash_folder); res.insert("mdns_enabled", mdns_enabled.to_string()); res.insert("e2ee_enabled", e2ee_enabled.to_string()); - res.insert( - "key_gen_type", - self.get_config_int(Config::KeyGenType).await?.to_string(), - ); res.insert("bcc_self", bcc_self.to_string()); res.insert("sync_msgs", sync_msgs.to_string()); res.insert("disable_idle", disable_idle.to_string()); @@ -1077,21 +1061,21 @@ impl Context { ) .await? .unwrap_or_default(); - res += &format!("num_msgs {}\n", num_msgs); + res += &format!("num_msgs {num_msgs}\n"); let num_chats: u32 = self .sql .query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ()) .await? .unwrap_or_default(); - res += &format!("num_chats {}\n", num_chats); + res += &format!("num_chats {num_chats}\n"); let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len(); - res += &format!("db_size_bytes {}\n", db_size); + res += &format!("db_size_bytes {db_size}\n"); let secret_key = &load_self_secret_key(self).await?.primary_key; let key_created = secret_key.created_at().timestamp(); - res += &format!("key_created {}\n", key_created); + res += &format!("key_created {key_created}\n"); // how many of the chats active in the last months are: // - protected @@ -1171,7 +1155,7 @@ impl Context { id } }; - res += &format!("self_reporting_id {}", self_reporting_id); + res += &format!("self_reporting_id {self_reporting_id}"); Ok(res) } @@ -1480,653 +1464,4 @@ pub fn get_version_str() -> &'static str { } #[cfg(test)] -mod tests { - 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::chatlist::Chatlist; - use crate::constants::Chattype; - 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}; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_wrong_db() -> Result<()> { - let tmp = tempfile::tempdir()?; - let dbfile = tmp.path().join("db.sqlite"); - tokio::fs::write(&dbfile, b"123").await?; - let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?; - - // Broken database is indistinguishable from encrypted one. - assert_eq!(res.is_open().await, false); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_fresh_msgs() { - let t = TestContext::new().await; - let fresh = t.get_fresh_msgs().await.unwrap(); - assert!(fresh.is_empty()) - } - - async fn receive_msg(t: &TestContext, chat: &Chat) { - let members = get_chat_contacts(t, chat.id).await.unwrap(); - let contact = Contact::get_by_id(t, *members.first().unwrap()) - .await - .unwrap(); - let msg = format!( - "From: {}\n\ - To: alice@example.org\n\ - Message-ID: <{}>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - contact.get_addr(), - create_outgoing_rfc724_mid() - ); - println!("{msg}"); - receive_imf(t, msg.as_bytes(), false).await.unwrap(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_fresh_msgs_and_muted_chats() { - // receive various mails in 3 chats - let t = TestContext::new_alice().await; - let bob = t.create_chat_with_contact("", "bob@g.it").await; - let claire = t.create_chat_with_contact("", "claire@g.it").await; - let dave = t.create_chat_with_contact("", "dave@g.it").await; - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); - - receive_msg(&t, &bob).await; - assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1); - assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - - receive_msg(&t, &claire).await; - receive_msg(&t, &claire).await; - assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3); - - receive_msg(&t, &dave).await; - receive_msg(&t, &dave).await; - receive_msg(&t, &dave).await; - assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3); - assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); - - // mute one of the chats - set_muted(&t, claire.id, MuteDuration::Forever) - .await - .unwrap(); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted - - // receive more messages - receive_msg(&t, &bob).await; - receive_msg(&t, &claire).await; - receive_msg(&t, &dave).await; - assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted - - // unmute claire again - set_muted(&t, claire.id, MuteDuration::NotMuted) - .await - .unwrap(); - assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_fresh_msgs_and_muted_until() { - let t = TestContext::new_alice().await; - let bob = t.create_chat_with_contact("", "bob@g.it").await; - receive_msg(&t, &bob).await; - assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1); - - // chat is unmuted by default, here and in the following assert(), - // we check mainly that the SQL-statements in is_muted() and get_fresh_msgs() - // have the same view to the database. - assert!(!bob.is_muted()); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - - // test get_fresh_msgs() with mute_until in the future - set_muted( - &t, - bob.id, - MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)), - ) - .await - .unwrap(); - let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); - assert!(bob.is_muted()); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); - - // to test get_fresh_msgs() with mute_until in the past, - // we need to modify the database directly - t.sql - .execute( - "UPDATE chats SET muted_until=? WHERE id=?;", - (time() - 3600, bob.id), - ) - .await - .unwrap(); - let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); - assert!(!bob.is_muted()); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - - // test get_fresh_msgs() with "forever" mute_until - set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap(); - let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); - assert!(bob.is_muted()); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); - - // to test get_fresh_msgs() with invalid mute_until (everything < -1), - // that results in "muted forever" by definition. - t.sql - .execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,)) - .await - .unwrap(); - let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); - assert!(!bob.is_muted()); - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_muted_context() -> Result<()> { - let t = TestContext::new_alice().await; - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); - t.set_config(Config::IsMuted, Some("1")).await?; - let chat = t.create_chat_with_contact("", "bob@g.it").await; - receive_msg(&t, &chat).await; - - // muted contexts should still show dimmed badge counters eg. in the sidebars, - // (same as muted chats show dimmed badge counters in the chatlist) - // therefore the fresh messages count should not be affected. - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_blobdir_exists() { - let tmp = tempfile::tempdir().unwrap(); - let dbfile = tmp.path().join("db.sqlite"); - Context::new(&dbfile, 1, Events::new(), StockStrings::new()) - .await - .unwrap(); - let blobdir = tmp.path().join("db.sqlite-blobs"); - assert!(blobdir.is_dir()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_wrong_blogdir() { - let tmp = tempfile::tempdir().unwrap(); - let dbfile = tmp.path().join("db.sqlite"); - let blobdir = tmp.path().join("db.sqlite-blobs"); - tokio::fs::write(&blobdir, b"123").await.unwrap(); - let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await; - assert!(res.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sqlite_parent_not_exists() { - let tmp = tempfile::tempdir().unwrap(); - let subdir = tmp.path().join("subdir"); - let dbfile = subdir.join("db.sqlite"); - let dbfile2 = dbfile.clone(); - Context::new(&dbfile, 1, Events::new(), StockStrings::new()) - .await - .unwrap(); - assert!(subdir.is_dir()); - assert!(dbfile2.is_file()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_with_empty_blobdir() { - 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(), - ); - assert!(res.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_with_blobdir_not_exists() { - 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(), - ); - assert!(res.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn no_crashes_on_context_deref() { - let t = TestContext::new().await; - std::mem::drop(t); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_info() { - let t = TestContext::new().await; - - let info = t.get_info().await.unwrap(); - assert!(info.contains_key("database_dir")); - } - - #[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_eq!(info.get("level").unwrap(), "awesome"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_info_completeness() { - // For easier debugging, - // get_info() shall return all important information configurable by the Config-values. - // - // There are exceptions for Config-values considered to be unimportant, - // too sensitive or summarized in another item. - let skip_from_get_info = vec![ - "addr", - "displayname", - "imap_certificate_checks", - "mail_server", - "mail_user", - "mail_pw", - "mail_port", - "mail_security", - "notify_about_wrong_pw", - "self_reporting_id", - "selfstatus", - "send_server", - "send_user", - "send_pw", - "send_port", - "send_security", - "server_flags", - "skip_start_messages", - "smtp_certificate_checks", - "proxy_url", // May contain passwords, don't leak it to the logs. - "socks5_enabled", // SOCKS5 options are deprecated. - "socks5_host", - "socks5_port", - "socks5_user", - "socks5_password", - "key_id", - "webxdc_integration", - "device_token", - ]; - let t = TestContext::new().await; - let info = t.get_info().await.unwrap(); - for key in Config::iter() { - let key: String = key.to_string(); - if !skip_from_get_info.contains(&&*key) - && !key.starts_with("configured") - && !key.starts_with("sys.") - { - assert!( - info.contains_key(&*key), - "'{key}' missing in get_info() output" - ); - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_search_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.org") - .await; - - // Global search finds nothing. - let res = alice.search_msgs(None, "foo").await?; - assert!(res.is_empty()); - - // Search in chat with Bob finds nothing. - let res = alice.search_msgs(Some(chat.id), "foo").await?; - assert!(res.is_empty()); - - // Add messages to chat with Bob. - let mut msg1 = Message::new_text("foobar".to_string()); - send_msg(&alice, chat.id, &mut msg1).await?; - - let mut msg2 = Message::new_text("barbaz".to_string()); - send_msg(&alice, chat.id, &mut msg2).await?; - - alice.send_text(chat.id, "Δ-Chat").await; - - // Global search with a part of text finds the message. - let res = alice.search_msgs(None, "ob").await?; - assert_eq!(res.len(), 1); - - // Global search for "bar" matches both "foobar" and "barbaz". - let res = alice.search_msgs(None, "bar").await?; - assert_eq!(res.len(), 2); - - // Message added later is returned first. - assert_eq!(res.first(), Some(&msg2.id)); - assert_eq!(res.get(1), Some(&msg1.id)); - - // Search is case-insensitive. - for chat_id in [None, Some(chat.id)] { - let res = alice.search_msgs(chat_id, "δ-chat").await?; - assert_eq!(res.len(), 1); - } - - // Global search with longer text does not find any message. - let res = alice.search_msgs(None, "foobarbaz").await?; - assert!(res.is_empty()); - - // Search for random string finds nothing. - let res = alice.search_msgs(None, "abc").await?; - assert!(res.is_empty()); - - // Search in chat with Bob finds the message. - let res = alice.search_msgs(Some(chat.id), "foo").await?; - assert_eq!(res.len(), 1); - - // Search in Saved Messages does not find the message. - let res = alice.search_msgs(Some(self_talk), "foo").await?; - assert!(res.is_empty()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_search_unaccepted_requests() -> Result<()> { - let t = TestContext::new_alice().await; - receive_imf( - &t, - b"From: BobBar \n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Date: Tue, 25 Oct 2022 13:37:00 +0000\n\ - \n\ - hello bob, foobar test!\n", - false, - ) - .await?; - let chat_id = t.get_last_msg().await.get_chat_id(); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.get_type(), Chattype::Single); - assert!(chat.is_contact_request()); - - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1); - assert_eq!( - Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), - 1 - ); - assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1); - assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1); - - chat_id.block(&t).await?; - - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); - assert_eq!( - Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), - 0 - ); - assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0); - assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0); - - let contact_ids = get_chat_contacts(&t, chat_id).await?; - Contact::unblock(&t, *contact_ids.first().unwrap()).await?; - - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1); - assert_eq!( - Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), - 1 - ); - assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1); - assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_limit_search_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.org") - .await; - - // Add 999 messages - let mut msg = Message::new_text("foobar".to_string()); - for _ in 0..999 { - send_msg(&alice, chat.id, &mut msg).await?; - } - let res = alice.search_msgs(None, "foo").await?; - assert_eq!(res.len(), 999); - - // Add one more message, no limit yet - send_msg(&alice, chat.id, &mut msg).await?; - let res = alice.search_msgs(None, "foo").await?; - assert_eq!(res.len(), 1000); - - // Add one more message, that one is truncated then - send_msg(&alice, chat.id, &mut msg).await?; - let res = alice.search_msgs(None, "foo").await?; - assert_eq!(res.len(), 1000); - - // In-chat should not be not limited - let res = alice.search_msgs(Some(chat.id), "foo").await?; - assert_eq!(res.len(), 1001); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_check_passphrase() -> Result<()> { - let dir = tempdir()?; - let dbfile = dir.path().join("db.sqlite"); - - let context = ContextBuilder::new(dbfile.clone()) - .with_id(1) - .build() - .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() - .await - .context("failed to create context")?; - assert_eq!(context.is_open().await, false); - assert_eq!(context.check_passphrase("bar".to_string()).await?, false); - assert_eq!(context.open("false".to_string()).await?, false); - assert_eq!(context.open("foo".to_string()).await?, true); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_context_change_passphrase() -> Result<()> { - let dir = tempdir()?; - let dbfile = dir.path().join("db.sqlite"); - - let context = ContextBuilder::new(dbfile) - .with_id(1) - .build() - .await - .context("failed to create context")?; - assert_eq!(context.open("foo".to_string()).await?, true); - assert_eq!(context.is_open().await, true); - - context - .set_config(Config::Addr, Some("alice@example.org")) - .await?; - - context - .change_passphrase("bar".to_string()) - .await - .context("Failed to change passphrase")?; - - assert_eq!( - context.get_config(Config::Addr).await?.unwrap(), - "alice@example.org" - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ongoing() -> Result<()> { - let context = TestContext::new().await; - - // No ongoing process allocated. - assert!(context.shall_stop_ongoing().await); - - let receiver = context.alloc_ongoing().await?; - - // Cannot allocate another ongoing process while the first one is running. - assert!(context.alloc_ongoing().await.is_err()); - - // Stop signal is not sent yet. - assert!(receiver.try_recv().is_err()); - - assert!(!context.shall_stop_ongoing().await); - - // Send the stop signal. - context.stop_ongoing().await; - - // Receive stop signal. - receiver.recv().await?; - - assert!(context.shall_stop_ongoing().await); - - // Ongoing process is still running even though stop signal was received, - // so another one cannot be allocated. - assert!(context.alloc_ongoing().await.is_err()); - - context.free_ongoing().await; - - // No ongoing process allocated, should have been stopped already. - assert!(context.shall_stop_ongoing().await); - - // Another ongoing process can be allocated now. - let _receiver = context.alloc_ongoing().await?; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_next_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let alice_chat = alice.create_chat(&bob).await; - - assert!(alice.get_next_msgs().await?.is_empty()); - assert!(bob.get_next_msgs().await?.is_empty()); - - let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; - let received_msg = bob.recv_msg(&sent_msg).await; - - let bob_next_msg_ids = bob.get_next_msgs().await?; - assert_eq!(bob_next_msg_ids.len(), 1); - assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id)); - - bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32()) - .await?; - assert!(bob.get_next_msgs().await?.is_empty()); - - // Next messages include self-sent messages. - let alice_next_msg_ids = alice.get_next_msgs().await?; - assert_eq!(alice_next_msg_ids.len(), 1); - assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id)); - - alice - .set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32()) - .await?; - assert!(alice.get_next_msgs().await?.is_empty()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_draft_self_report() -> Result<()> { - let alice = TestContext::new_alice().await; - - let chat_id = alice.draft_self_report().await?; - let msg = get_chat_msg(&alice, chat_id, 0, 1).await; - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - - let chat = Chat::load_from_db(&alice, chat_id).await?; - assert!(chat.is_protected()); - - let mut draft = chat_id.get_draft(&alice).await?.unwrap(); - assert!(draft.text.starts_with("core_version")); - - // Test that sending into the protected chat works: - let _sent = alice.send_msg(chat_id, &mut draft).await; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_cache_is_cleared_when_io_is_started() -> Result<()> { - let alice = TestContext::new_alice().await; - assert_eq!( - alice.get_config(Config::ShowEmails).await?, - Some("2".to_string()) - ); - - // Change the config circumventing the cache - // This simulates what the notification plugin on iOS might do - // because it runs in a different process - alice - .sql - .execute( - "INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')", - (), - ) - .await?; - - // Alice's Delta Chat doesn't know about it yet: - assert_eq!( - alice.get_config(Config::ShowEmails).await?, - Some("2".to_string()) - ); - - // Starting IO will fail of course because no server settings are configured, - // but it should invalidate the caches: - alice.start_io().await; - - assert_eq!( - alice.get_config(Config::ShowEmails).await?, - Some("0".to_string()) - ); - - Ok(()) - } -} +mod context_tests; diff --git a/src/context/context_tests.rs b/src/context/context_tests.rs new file mode 100644 index 0000000000..111311aca2 --- /dev/null +++ b/src/context/context_tests.rs @@ -0,0 +1,649 @@ +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::chatlist::Chatlist; +use crate::constants::Chattype; +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}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_wrong_db() -> Result<()> { + let tmp = tempfile::tempdir()?; + let dbfile = tmp.path().join("db.sqlite"); + tokio::fs::write(&dbfile, b"123").await?; + let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?; + + // Broken database is indistinguishable from encrypted one. + assert_eq!(res.is_open().await, false); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_fresh_msgs() { + let t = TestContext::new().await; + let fresh = t.get_fresh_msgs().await.unwrap(); + assert!(fresh.is_empty()) +} + +async fn receive_msg(t: &TestContext, chat: &Chat) { + let members = get_chat_contacts(t, chat.id).await.unwrap(); + let contact = Contact::get_by_id(t, *members.first().unwrap()) + .await + .unwrap(); + let msg = format!( + "From: {}\n\ + To: alice@example.org\n\ + Message-ID: <{}>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + contact.get_addr(), + create_outgoing_rfc724_mid() + ); + println!("{msg}"); + receive_imf(t, msg.as_bytes(), false).await.unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_fresh_msgs_and_muted_chats() { + // receive various mails in 3 chats + let t = TestContext::new_alice().await; + let bob = t.create_chat_with_contact("", "bob@g.it").await; + let claire = t.create_chat_with_contact("", "claire@g.it").await; + let dave = t.create_chat_with_contact("", "dave@g.it").await; + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); + + receive_msg(&t, &bob).await; + assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1); + assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); + + receive_msg(&t, &claire).await; + receive_msg(&t, &claire).await; + assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3); + + receive_msg(&t, &dave).await; + receive_msg(&t, &dave).await; + receive_msg(&t, &dave).await; + assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3); + assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); + + // mute one of the chats + set_muted(&t, claire.id, MuteDuration::Forever) + .await + .unwrap(); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted + + // receive more messages + receive_msg(&t, &bob).await; + receive_msg(&t, &claire).await; + receive_msg(&t, &dave).await; + assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted + + // unmute claire again + set_muted(&t, claire.id, MuteDuration::NotMuted) + .await + .unwrap(); + assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_fresh_msgs_and_muted_until() { + let t = TestContext::new_alice().await; + let bob = t.create_chat_with_contact("", "bob@g.it").await; + receive_msg(&t, &bob).await; + assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1); + + // chat is unmuted by default, here and in the following assert(), + // we check mainly that the SQL-statements in is_muted() and get_fresh_msgs() + // have the same view to the database. + assert!(!bob.is_muted()); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); + + // test get_fresh_msgs() with mute_until in the future + set_muted( + &t, + bob.id, + MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)), + ) + .await + .unwrap(); + let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); + assert!(bob.is_muted()); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); + + // to test get_fresh_msgs() with mute_until in the past, + // we need to modify the database directly + t.sql + .execute( + "UPDATE chats SET muted_until=? WHERE id=?;", + (time() - 3600, bob.id), + ) + .await + .unwrap(); + let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); + assert!(!bob.is_muted()); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); + + // test get_fresh_msgs() with "forever" mute_until + set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap(); + let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); + assert!(bob.is_muted()); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); + + // to test get_fresh_msgs() with invalid mute_until (everything < -1), + // that results in "muted forever" by definition. + t.sql + .execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,)) + .await + .unwrap(); + let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); + assert!(!bob.is_muted()); + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_muted_context() -> Result<()> { + let t = TestContext::new_alice().await; + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0); + t.set_config(Config::IsMuted, Some("1")).await?; + let chat = t.create_chat_with_contact("", "bob@g.it").await; + receive_msg(&t, &chat).await; + + // muted contexts should still show dimmed badge counters eg. in the sidebars, + // (same as muted chats show dimmed badge counters in the chatlist) + // therefore the fresh messages count should not be affected. + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_blobdir_exists() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + Context::new(&dbfile, 1, Events::new(), StockStrings::new()) + .await + .unwrap(); + let blobdir = tmp.path().join("db.sqlite-blobs"); + assert!(blobdir.is_dir()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_wrong_blogdir() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + let blobdir = tmp.path().join("db.sqlite-blobs"); + tokio::fs::write(&blobdir, b"123").await.unwrap(); + let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await; + assert!(res.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sqlite_parent_not_exists() { + let tmp = tempfile::tempdir().unwrap(); + let subdir = tmp.path().join("subdir"); + let dbfile = subdir.join("db.sqlite"); + let dbfile2 = dbfile.clone(); + Context::new(&dbfile, 1, Events::new(), StockStrings::new()) + .await + .unwrap(); + assert!(subdir.is_dir()); + assert!(dbfile2.is_file()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_with_empty_blobdir() { + 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(), + ); + assert!(res.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_with_blobdir_not_exists() { + 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(), + ); + assert!(res.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn no_crashes_on_context_deref() { + let t = TestContext::new().await; + std::mem::drop(t); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_info() { + let t = TestContext::new().await; + + let info = t.get_info().await.unwrap(); + assert!(info.contains_key("database_dir")); +} + +#[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_eq!(info.get("level").unwrap(), "awesome"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_info_completeness() { + // For easier debugging, + // get_info() shall return all important information configurable by the Config-values. + // + // There are exceptions for Config-values considered to be unimportant, + // too sensitive or summarized in another item. + let skip_from_get_info = vec![ + "addr", + "displayname", + "imap_certificate_checks", + "mail_server", + "mail_user", + "mail_pw", + "mail_port", + "mail_security", + "notify_about_wrong_pw", + "self_reporting_id", + "selfstatus", + "send_server", + "send_user", + "send_pw", + "send_port", + "send_security", + "server_flags", + "skip_start_messages", + "smtp_certificate_checks", + "proxy_url", // May contain passwords, don't leak it to the logs. + "socks5_enabled", // SOCKS5 options are deprecated. + "socks5_host", + "socks5_port", + "socks5_user", + "socks5_password", + "key_id", + "webxdc_integration", + "device_token", + "encrypted_device_token", + ]; + let t = TestContext::new().await; + let info = t.get_info().await.unwrap(); + for key in Config::iter() { + let key: String = key.to_string(); + if !skip_from_get_info.contains(&&*key) + && !key.starts_with("configured") + && !key.starts_with("sys.") + { + assert!( + info.contains_key(&*key), + "'{key}' missing in get_info() output" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_search_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?; + let chat = alice + .create_chat_with_contact("Bob", "bob@example.org") + .await; + + // Global search finds nothing. + let res = alice.search_msgs(None, "foo").await?; + assert!(res.is_empty()); + + // Search in chat with Bob finds nothing. + let res = alice.search_msgs(Some(chat.id), "foo").await?; + assert!(res.is_empty()); + + // Add messages to chat with Bob. + let mut msg1 = Message::new_text("foobar".to_string()); + send_msg(&alice, chat.id, &mut msg1).await?; + + let mut msg2 = Message::new_text("barbaz".to_string()); + send_msg(&alice, chat.id, &mut msg2).await?; + + alice.send_text(chat.id, "Δ-Chat").await; + + // Global search with a part of text finds the message. + let res = alice.search_msgs(None, "ob").await?; + assert_eq!(res.len(), 1); + + // Global search for "bar" matches both "foobar" and "barbaz". + let res = alice.search_msgs(None, "bar").await?; + assert_eq!(res.len(), 2); + + // Message added later is returned first. + assert_eq!(res.first(), Some(&msg2.id)); + assert_eq!(res.get(1), Some(&msg1.id)); + + // Search is case-insensitive. + for chat_id in [None, Some(chat.id)] { + let res = alice.search_msgs(chat_id, "δ-chat").await?; + assert_eq!(res.len(), 1); + } + + // Global search with longer text does not find any message. + let res = alice.search_msgs(None, "foobarbaz").await?; + assert!(res.is_empty()); + + // Search for random string finds nothing. + let res = alice.search_msgs(None, "abc").await?; + assert!(res.is_empty()); + + // Search in chat with Bob finds the message. + let res = alice.search_msgs(Some(chat.id), "foo").await?; + assert_eq!(res.len(), 1); + + // Search in Saved Messages does not find the message. + let res = alice.search_msgs(Some(self_talk), "foo").await?; + assert!(res.is_empty()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_search_unaccepted_requests() -> Result<()> { + let t = TestContext::new_alice().await; + receive_imf( + &t, + b"From: BobBar \n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Date: Tue, 25 Oct 2022 13:37:00 +0000\n\ + \n\ + hello bob, foobar test!\n", + false, + ) + .await?; + let chat_id = t.get_last_msg().await.get_chat_id(); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.get_type(), Chattype::Single); + assert!(chat.is_contact_request()); + + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1); + assert_eq!( + Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), + 1 + ); + assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1); + assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1); + + chat_id.block(&t).await?; + + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); + assert_eq!( + Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), + 0 + ); + assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0); + assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0); + + let contact_ids = get_chat_contacts(&t, chat_id).await?; + Contact::unblock(&t, *contact_ids.first().unwrap()).await?; + + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1); + assert_eq!( + Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(), + 1 + ); + assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1); + assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_limit_search_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat = alice + .create_chat_with_contact("Bob", "bob@example.org") + .await; + + // Add 999 messages + let mut msg = Message::new_text("foobar".to_string()); + for _ in 0..999 { + send_msg(&alice, chat.id, &mut msg).await?; + } + let res = alice.search_msgs(None, "foo").await?; + assert_eq!(res.len(), 999); + + // Add one more message, no limit yet + send_msg(&alice, chat.id, &mut msg).await?; + let res = alice.search_msgs(None, "foo").await?; + assert_eq!(res.len(), 1000); + + // Add one more message, that one is truncated then + send_msg(&alice, chat.id, &mut msg).await?; + let res = alice.search_msgs(None, "foo").await?; + assert_eq!(res.len(), 1000); + + // In-chat should not be not limited + let res = alice.search_msgs(Some(chat.id), "foo").await?; + assert_eq!(res.len(), 1001); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_check_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let context = ContextBuilder::new(dbfile.clone()) + .with_id(1) + .build() + .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() + .await + .context("failed to create context")?; + assert_eq!(context.is_open().await, false); + assert_eq!(context.check_passphrase("bar".to_string()).await?, false); + assert_eq!(context.open("false".to_string()).await?, false); + assert_eq!(context.open("foo".to_string()).await?, true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_context_change_passphrase() -> Result<()> { + let dir = tempdir()?; + let dbfile = dir.path().join("db.sqlite"); + + let context = ContextBuilder::new(dbfile) + .with_id(1) + .build() + .await + .context("failed to create context")?; + assert_eq!(context.open("foo".to_string()).await?, true); + assert_eq!(context.is_open().await, true); + + context + .set_config(Config::Addr, Some("alice@example.org")) + .await?; + + context + .change_passphrase("bar".to_string()) + .await + .context("Failed to change passphrase")?; + + assert_eq!( + context.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ongoing() -> Result<()> { + let context = TestContext::new().await; + + // No ongoing process allocated. + assert!(context.shall_stop_ongoing().await); + + let receiver = context.alloc_ongoing().await?; + + // Cannot allocate another ongoing process while the first one is running. + assert!(context.alloc_ongoing().await.is_err()); + + // Stop signal is not sent yet. + assert!(receiver.try_recv().is_err()); + + assert!(!context.shall_stop_ongoing().await); + + // Send the stop signal. + context.stop_ongoing().await; + + // Receive stop signal. + receiver.recv().await?; + + assert!(context.shall_stop_ongoing().await); + + // Ongoing process is still running even though stop signal was received, + // so another one cannot be allocated. + assert!(context.alloc_ongoing().await.is_err()); + + context.free_ongoing().await; + + // No ongoing process allocated, should have been stopped already. + assert!(context.shall_stop_ongoing().await); + + // Another ongoing process can be allocated now. + let _receiver = context.alloc_ongoing().await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_next_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let alice_chat = alice.create_chat(&bob).await; + + assert!(alice.get_next_msgs().await?.is_empty()); + assert!(bob.get_next_msgs().await?.is_empty()); + + let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await; + let received_msg = bob.recv_msg(&sent_msg).await; + + let bob_next_msg_ids = bob.get_next_msgs().await?; + assert_eq!(bob_next_msg_ids.len(), 1); + assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id)); + + bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32()) + .await?; + assert!(bob.get_next_msgs().await?.is_empty()); + + // Next messages include self-sent messages. + let alice_next_msg_ids = alice.get_next_msgs().await?; + assert_eq!(alice_next_msg_ids.len(), 1); + assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id)); + + alice + .set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32()) + .await?; + assert!(alice.get_next_msgs().await?.is_empty()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_draft_self_report() -> Result<()> { + let alice = TestContext::new_alice().await; + + let chat_id = alice.draft_self_report().await?; + let msg = get_chat_msg(&alice, chat_id, 0, 1).await; + assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); + + let chat = Chat::load_from_db(&alice, chat_id).await?; + assert!(chat.is_protected()); + + let mut draft = chat_id.get_draft(&alice).await?.unwrap(); + assert!(draft.text.starts_with("core_version")); + + // Test that sending into the protected chat works: + let _sent = alice.send_msg(chat_id, &mut draft).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_cache_is_cleared_when_io_is_started() -> Result<()> { + let alice = TestContext::new_alice().await; + assert_eq!( + alice.get_config(Config::ShowEmails).await?, + Some("2".to_string()) + ); + + // Change the config circumventing the cache + // This simulates what the notification plugin on iOS might do + // because it runs in a different process + alice + .sql + .execute( + "INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')", + (), + ) + .await?; + + // Alice's Delta Chat doesn't know about it yet: + assert_eq!( + alice.get_config(Config::ShowEmails).await?, + Some("2".to_string()) + ); + + // Starting IO will fail of course because no server settings are configured, + // but it should invalidate the caches: + alice.start_io().await; + + assert_eq!( + alice.get_config(Config::ShowEmails).await?, + Some("0".to_string()) + ); + + Ok(()) +} diff --git a/src/debug_logging.rs b/src/debug_logging.rs index 4b0ce441f4..9b437e8cc1 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -9,7 +9,6 @@ use crate::tools::time; use crate::webxdc::StatusUpdateItem; use async_channel::{self as channel, Receiver, Sender}; use serde_json::json; -use std::path::PathBuf; use tokio::task; #[derive(Debug)] @@ -100,9 +99,7 @@ pub async fn maybe_set_logging_xdc( context, msg.get_viewtype(), chat_id, - msg.param - .get_path(Param::Filename, context) - .unwrap_or_default(), + msg.param.get(Param::Filename), msg.get_id(), ) .await?; @@ -115,18 +112,16 @@ pub async fn maybe_set_logging_xdc_inner( context: &Context, viewtype: Viewtype, chat_id: ChatId, - filename: Option, + filename: Option<&str>, msg_id: MsgId, ) -> anyhow::Result<()> { if viewtype == Viewtype::Webxdc { - if let Some(file) = filename { - if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) { - if file_name.starts_with("debug_logging") - && file_name.ends_with(".xdc") - && chat_id.is_self_talk(context).await? - { - set_debug_logging_xdc(context, Some(msg_id)).await?; - } + if let Some(filename) = filename { + if filename.starts_with("debug_logging") + && filename.ends_with(".xdc") + && chat_id.is_self_talk(context).await? + { + set_debug_logging_xdc(context, Some(msg_id)).await?; } } } diff --git a/src/dehtml.rs b/src/dehtml.rs index 8ae56240e0..b7f742ee08 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -3,8 +3,8 @@ //! A module to remove HTML tags from the email text use std::io::BufRead; +use std::sync::LazyLock; -use once_cell::sync::Lazy; use quick_xml::{ events::{BytesEnd, BytesStart, BytesText}, Reader, @@ -176,7 +176,8 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) { } fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) { - static LINE_RE: Lazy = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap()); + static LINE_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap()); if dehtml.get_add_text() == AddText::YesPreserveLineEnds || dehtml.get_add_text() == AddText::YesRemoveLineEnds diff --git a/src/download.rs b/src/download.rs index c8a75227a0..f1f3647749 100644 --- a/src/download.rs +++ b/src/download.rs @@ -220,7 +220,6 @@ impl Session { vec![uid], &uid_message_ids, false, - false, ) .await?; if last_uid.is_none() { @@ -369,7 +368,6 @@ mod tests { header.as_bytes(), false, Some(100000), - false, ) .await?; let msg = t.get_last_msg().await; @@ -385,7 +383,6 @@ mod tests { format!("{header}\n\n100k text...").as_bytes(), false, None, - false, ) .await?; let msg = t.get_last_msg().await; @@ -420,7 +417,6 @@ mod tests { Content-Type: text/plain", false, Some(100000), - false, ) .await?; assert_eq!( @@ -457,7 +453,6 @@ mod tests { sent2.payload().as_bytes(), false, Some(sent2.payload().len() as u32), - false, ) .await?; let msg = bob.get_last_msg().await; @@ -473,7 +468,6 @@ mod tests { sent2.payload().as_bytes(), false, None, - false, ) .await?; assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0); @@ -517,15 +511,7 @@ mod tests { "; // not downloading the mdn results in an placeholder - receive_imf_from_inbox( - &bob, - "bar@example.org", - raw, - false, - Some(raw.len() as u32), - false, - ) - .await?; + receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?; let msg = bob.get_last_msg().await; let chat_id = msg.chat_id; assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1); @@ -533,7 +519,7 @@ mod tests { // downloading the mdn afterwards expands to nothing and deletes the placeholder directly // (usually mdn are too small for not being downloaded directly) - receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?; + receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?; assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0); assert!(Message::load_from_db_optional(&bob, msg.id) .await? diff --git a/src/e2ee.rs b/src/e2ee.rs index 70c835cf2b..011e8b8c06 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -1,6 +1,10 @@ //! End-to-end encryption support. -use anyhow::{format_err, Context as _, Result}; +use std::collections::BTreeSet; +use std::io::Cursor; + +use anyhow::{bail, Result}; +use mail_builder::mime::MimePart; use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; @@ -39,88 +43,76 @@ impl EncryptHelper { } /// Determines if we can and should encrypt. - /// - /// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by - /// Autocrypt Level 1, version 1.1) and for messages sent in protected groups. - /// - /// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing. pub(crate) async fn should_encrypt( &self, context: &Context, - e2ee_guaranteed: bool, peerstates: &[(Option, String)], ) -> Result { let is_chatmail = context.is_chatmail().await?; - let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual { - 1 - } else { - 0 - }; - for (peerstate, addr) in peerstates { - match peerstate { - Some(peerstate) => { - let prefer_encrypt = peerstate.prefer_encrypt; - info!(context, "Peerstate for {addr:?} is {prefer_encrypt}."); - if match peerstate.prefer_encrypt { - EncryptPreference::NoPreference | EncryptPreference::Reset => { - (peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail) - && self.prefer_encrypt == EncryptPreference::Mutual - } - EncryptPreference::Mutual => true, - } { - prefer_encrypt_count += 1; - } - } - None => { - let msg = format!("Peerstate for {addr:?} missing, cannot encrypt"); - if e2ee_guaranteed { - return Err(format_err!("{msg}")); - } else { - info!(context, "{msg}."); - return Ok(false); - } + for (peerstate, _addr) in peerstates { + if let Some(peerstate) = peerstate { + // For chatmail we ignore the encryption preference, + // because we can either send encrypted or not at all. + if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset { + continue; } } + return Ok(false); } - - // Count number of recipients, including self. - // This does not depend on whether we send a copy to self or not. - let recipients_count = peerstates.len() + 1; - - Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count) + Ok(true) } - /// Tries to encrypt the passed in `mail`. - pub async fn encrypt( - self, + /// Constructs a vector of public keys for given peerstates. + /// + /// In addition returns the set of recipient addresses + /// for which there is no key available. + /// + /// Returns an error if there are recipients + /// other than self, but no recipient keys are available. + pub(crate) fn encryption_keyring( + &self, context: &Context, verified: bool, - mail_to_encrypt: lettre_email::PartBuilder, - peerstates: Vec<(Option, String)>, - compress: bool, - ) -> Result { - let mut keyring: Vec = Vec::new(); + peerstates: &[(Option, String)], + ) -> Result<(Vec, BTreeSet)> { + // Encrypt to self unconditionally, + // even for a single-device setup. + let mut keyring = vec![self.public_key.clone()]; + let mut missing_key_addresses = BTreeSet::new(); + + if peerstates.is_empty() { + return Ok((keyring, missing_key_addresses)); + } let mut verifier_addresses: Vec<&str> = Vec::new(); - for (peerstate, addr) in peerstates - .iter() - .filter_map(|(state, addr)| state.clone().map(|s| (s, addr))) - { - let key = peerstate - .take_key(verified) - .with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?; - keyring.push(key); - verifier_addresses.push(addr); + for (peerstate, addr) in peerstates { + if let Some(peerstate) = peerstate { + if let Some(key) = peerstate.clone().take_key(verified) { + keyring.push(key); + verifier_addresses.push(addr); + } else { + warn!(context, "Encryption key for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + } + } else { + warn!(context, "Peerstate for {addr} is missing."); + missing_key_addresses.insert(addr.clone()); + } } - // Encrypt to self. - keyring.push(self.public_key.clone()); + debug_assert!( + !keyring.is_empty(), + "At least our own key is in the keyring" + ); + if keyring.len() <= 1 { + bail!("No recipient keys are available, cannot encrypt"); + } // Encrypt to secondary verified keys // if we also encrypt to the introducer ("verifier") of the key. if verified { - for (peerstate, _addr) in &peerstates { + for (peerstate, _addr) in peerstates { if let Some(peerstate) = peerstate { if let (Some(key), Some(verifier)) = ( peerstate.secondary_verified_key.as_ref(), @@ -134,9 +126,22 @@ impl EncryptHelper { } } + Ok((keyring, missing_key_addresses)) + } + + /// Tries to encrypt the passed in `mail`. + pub async fn encrypt( + self, + context: &Context, + keyring: Vec, + mail_to_encrypt: MimePart<'static>, + compress: bool, + ) -> Result { let sign_key = load_self_secret_key(context).await?; - let raw_message = mail_to_encrypt.build().as_string().into_bytes(); + let mut raw_message = Vec::new(); + let cursor = Cursor::new(&mut raw_message); + mail_to_encrypt.clone().write_part(cursor).ok(); let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?; @@ -145,15 +150,13 @@ impl EncryptHelper { /// Signs the passed-in `mail` using the private key from `context`. /// Returns the payload and the signature. - pub async fn sign( - self, - context: &Context, - mail: lettre_email::PartBuilder, - ) -> Result<(lettre_email::MimeMessage, String)> { + pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result { let sign_key = load_self_secret_key(context).await?; - let mime_message = mail.build(); - let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?; - Ok((mime_message, signature)) + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + mail.clone().write_part(cursor).ok(); + let signature = pgp::pk_calc_signature(&buffer, &sign_key)?; + Ok(signature) } } @@ -173,6 +176,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> { mod tests { use super::*; use crate::chat::send_text_msg; + use crate::config::Config; use crate::key::DcKey; use crate::message::{Message, Viewtype}; use crate::param::Param; @@ -225,8 +229,8 @@ Sent with my Delta Chat Messenger: https://delta.chat"; let alice = tcm.alice().await; let bob = tcm.bob().await; - let chat_alice = alice.create_chat(&bob).await.id; - let chat_bob = bob.create_chat(&alice).await.id; + let chat_alice = alice.create_email_chat(&bob).await.id; + let chat_bob = bob.create_email_chat(&alice).await.id; // Alice sends unencrypted message to Bob let mut msg = Message::new(Viewtype::Text); @@ -326,81 +330,20 @@ Sent with my Delta Chat Messenger: https://delta.chat"; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_should_encrypt() -> Result<()> { let t = TestContext::new_alice().await; - assert!(t.get_config_bool(Config::E2eeEnabled).await?); let encrypt_helper = EncryptHelper::new(&t).await.unwrap(); let ps = new_peerstates(EncryptPreference::NoPreference); - assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?); - // Own preference is `Mutual` and we have the peer's key. - assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?); + assert!(encrypt_helper.should_encrypt(&t, &ps).await?); let ps = new_peerstates(EncryptPreference::Reset); - assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?); - assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?); + assert!(!encrypt_helper.should_encrypt(&t, &ps).await?); let ps = new_peerstates(EncryptPreference::Mutual); - assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?); - assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?); + assert!(encrypt_helper.should_encrypt(&t, &ps).await?); // test with missing peerstate let ps = vec![(None, "bob@foo.bar".to_string())]; - assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err()); - assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_should_encrypt_e2ee_disabled() -> Result<()> { - let t = &TestContext::new_alice().await; - t.set_config_bool(Config::E2eeEnabled, false).await?; - let encrypt_helper = EncryptHelper::new(t).await.unwrap(); - - let ps = new_peerstates(EncryptPreference::NoPreference); - assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?); - - let ps = new_peerstates(EncryptPreference::Reset); - assert!(encrypt_helper.should_encrypt(t, true, &ps).await?); - - let mut ps = new_peerstates(EncryptPreference::Mutual); - // Own preference is `NoPreference` and there's no majority with `Mutual`. - assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?); - // Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we - // can't send unencrypted, e.g. protected groups. - ps.push(ps[0].clone()); - assert!(encrypt_helper.should_encrypt(t, false, &ps).await?); - - // Test with missing peerstate. - let ps = vec![(None, "bob@foo.bar".to_string())]; - assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chatmail_prefers_to_encrypt() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - bob.set_config_bool(Config::IsChatmail, true).await?; - - let bob_chat_id = tcm - .send_recv_accept(alice, bob, "Hello from DC") - .await - .chat_id; - receive_imf( - bob, - b"From: alice@example.org\n\ - To: bob@example.net\n\ - Message-ID: <2222@example.org>\n\ - Date: Sun, 22 Mar 3000 22:37:58 +0000\n\ - \n\ - Hello from another MUA\n", - false, - ) - .await?; - send_text_msg(bob, bob_chat_id, "hi".to_string()).await?; - let sent_msg = bob.pop_sent_msg().await; - let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?; - assert!(msg.get_showpadlock()); + assert!(!encrypt_helper.should_encrypt(&t, &ps).await?); Ok(()) } diff --git a/src/ephemeral.rs b/src/ephemeral.rs index e0c00246ab..29143089b6 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -713,808 +713,4 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> { } #[cfg(test)] -mod tests { - use super::*; - use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration}; - use crate::config::Config; - use crate::constants::DC_CHAT_ID_ARCHIVED_LINK; - use crate::download::DownloadState; - use crate::location; - use crate::message::markseen_msgs; - use crate::receive_imf::receive_imf; - use crate::test_utils::{TestContext, TestContextManager}; - use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE; - use crate::{ - chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus}, - tools::IsNoneOrEmpty, - }; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_ephemeral_messages() { - let context = TestContext::new().await; - - assert_eq!( - stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await, - "You disabled message deletion timer." - ); - - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 1 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1 s." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 30 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 30 s." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 60 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1 minute." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 90 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1.5 minutes." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 30 * 60 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 30 minutes." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 60 * 60 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1 hour." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { duration: 5400 }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1.5 hours." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { - duration: 2 * 60 * 60 - }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 2 hours." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { - duration: 24 * 60 * 60 - }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1 day." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { - duration: 2 * 24 * 60 * 60 - }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 2 days." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { - duration: 7 * 24 * 60 * 60 - }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 1 week." - ); - assert_eq!( - stock_ephemeral_timer_changed( - &context, - Timer::Enabled { - duration: 4 * 7 * 24 * 60 * 60 - }, - ContactId::SELF - ) - .await, - "You set message deletion timer to 4 weeks." - ); - } - - /// Test enabling and disabling ephemeral timer remotely. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_enable_disable() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let chat_alice = alice.create_chat(&bob).await.id; - let chat_bob = bob.create_chat(&alice).await.id; - - chat_alice - .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 }) - .await?; - let sent = alice.pop_sent_msg().await; - bob.recv_msg(&sent).await; - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Enabled { duration: 60 } - ); - - chat_alice - .set_ephemeral_timer(&alice.ctx, Timer::Disabled) - .await?; - let sent = alice.pop_sent_msg().await; - bob.recv_msg(&sent).await; - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Disabled - ); - - Ok(()) - } - - /// Test that enabling ephemeral timer in unpromoted group does not send a message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_unpromoted() -> Result<()> { - let alice = TestContext::new_alice().await; - - let chat_id = - create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?; - - // Group is unpromoted, the timer can be changed without sending a message. - assert!(chat_id.is_unpromoted(&alice).await?); - chat_id - .set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 }) - .await?; - let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; - assert!(sent.is_none()); - assert_eq!( - chat_id.get_ephemeral_timer(&alice).await?, - Timer::Enabled { duration: 60 } - ); - - // Promote the group. - send_text_msg(&alice, chat_id, "hi!".to_string()).await?; - assert!(chat_id.is_promoted(&alice).await?); - let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; - assert!(sent.is_some()); - - chat_id - .set_ephemeral_timer(&alice.ctx, Timer::Disabled) - .await?; - let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; - assert!(sent.is_some()); - assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); - - Ok(()) - } - - /// Test that timer is enabled even if the message explicitly enabling the timer is lost. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_enable_lost() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let chat_alice = alice.create_chat(&bob).await.id; - let chat_bob = bob.create_chat(&alice).await.id; - - // Alice enables the timer. - chat_alice - .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 }) - .await?; - assert_eq!( - chat_alice.get_ephemeral_timer(&alice.ctx).await?, - Timer::Enabled { duration: 60 } - ); - // The message enabling the timer is lost. - let _sent = alice.pop_sent_msg().await; - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Disabled, - ); - - // Alice sends a text message. - let mut msg = Message::new(Viewtype::Text); - chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; - let sent = alice.pop_sent_msg().await; - - // Bob receives text message and enables the timer, even though explicit timer update was - // lost previously. - bob.recv_msg(&sent).await; - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Enabled { duration: 60 } - ); - - Ok(()) - } - - /// Test that Alice replying to the chat without a timer at the same time as Bob enables the - /// timer does not result in disabling the timer on the Bob's side. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_timer_rollback() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - let chat_alice = alice.create_chat(&bob).await.id; - let chat_bob = bob.create_chat(&alice).await.id; - - // Alice sends message to Bob - let mut msg = Message::new(Viewtype::Text); - chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; - let sent = alice.pop_sent_msg().await; - bob.recv_msg(&sent).await; - - // Alice sends second message to Bob, with no timer - let mut msg = Message::new(Viewtype::Text); - chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; - let sent = alice.pop_sent_msg().await; - - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Disabled - ); - - // Bob sets ephemeral timer and sends a message about timer change - chat_bob - .set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 }) - .await?; - let sent_timer_change = bob.pop_sent_msg().await; - - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Enabled { duration: 60 } - ); - - // Bob receives message from Alice. - // Alice message has no timer. However, Bob should not disable timer, - // because Alice replies to old message. - bob.recv_msg(&sent).await; - - assert_eq!( - chat_alice.get_ephemeral_timer(&alice.ctx).await?, - Timer::Disabled - ); - assert_eq!( - chat_bob.get_ephemeral_timer(&bob.ctx).await?, - Timer::Enabled { duration: 60 } - ); - - // Alice receives message from Bob - alice.recv_msg(&sent_timer_change).await; - - assert_eq!( - chat_alice.get_ephemeral_timer(&alice.ctx).await?, - Timer::Enabled { duration: 60 } - ); - - // Bob disables the chat timer. - // Note that the last message in the Bob's chat is from Alice and has no timer, - // but the chat timer is enabled. - chat_bob - .set_ephemeral_timer(&bob.ctx, Timer::Disabled) - .await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - assert_eq!( - chat_alice.get_ephemeral_timer(&alice.ctx).await?, - Timer::Disabled - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_delete_msgs() -> Result<()> { - let t = TestContext::new_alice().await; - let self_chat = t.get_self_chat().await; - - assert_eq!(next_expiration_timestamp(&t).await, None); - - t.send_text(self_chat.id, "Saved message, which we delete manually") - .await; - let msg = t.get_last_msg_in(self_chat.id).await; - msg.id.trash(&t, false).await?; - check_msg_is_deleted(&t, &self_chat, msg.id).await; - - self_chat - .id - .set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 }) - .await - .unwrap(); - - // Send a saved message which will be deleted after 3600s - let now = time(); - let msg = t.send_text(self_chat.id, "Message text").await; - - check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601) - .await - .unwrap(); - - // Set DeleteDeviceAfter to 1800s. Then send a saved message which will - // still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages. - t.set_config(Config::DeleteDeviceAfter, Some("1800")) - .await?; - - let now = time(); - let msg = t.send_text(self_chat.id, "Message text").await; - - check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601) - .await - .unwrap(); - - // Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter. - let bob_chat = t.create_chat_with_contact("", "bob@example.net").await; - let now = time(); - let msg = t.send_text(bob_chat.id, "Message text").await; - - check_msg_will_be_deleted( - &t, - msg.sender_msg_id, - &bob_chat, - now + 1799, - // The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and - // therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later. - time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE, - ) - .await - .unwrap(); - - // Enable ephemeral messages with Bob -> message will be deleted after 60s. - // This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time). - bob_chat - .id - .set_ephemeral_timer(&t, Timer::Enabled { duration: 60 }) - .await?; - - let now = time(); - let msg = t.send_text(bob_chat.id, "Message text").await; - - check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61) - .await - .unwrap(); - - Ok(()) - } - - async fn check_msg_will_be_deleted( - t: &TestContext, - msg_id: MsgId, - chat: &Chat, - not_deleted_at: i64, - deleted_at: i64, - ) -> Result<()> { - let next_expiration = next_expiration_timestamp(t).await.unwrap(); - - assert!(next_expiration > not_deleted_at); - delete_expired_messages(t, not_deleted_at).await?; - - let loaded = Message::load_from_db(t, msg_id).await?; - assert!(!loaded.text.is_empty()); - assert_eq!(loaded.chat_id, chat.id); - - assert!(next_expiration < deleted_at); - delete_expired_messages(t, deleted_at).await?; - t.evtracker - .get_matching(|evt| { - if let EventType::MsgDeleted { - msg_id: event_msg_id, - .. - } = evt - { - *event_msg_id == msg_id - } else { - false - } - }) - .await; - - let loaded = Message::load_from_db_optional(t, msg_id).await?; - assert!(loaded.is_none()); - - // Check that the msg was deleted locally. - check_msg_is_deleted(t, chat, msg_id).await; - - Ok(()) - } - - async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) { - let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap(); - // Check that the chat is empty except for possibly info messages: - for item in &chat_items { - if let ChatItem::Message { msg_id } = item { - let msg = Message::load_from_db(t, *msg_id).await.unwrap(); - assert!(msg.is_info()) - } - } - - // Check that if there is a message left, the text and metadata are gone - if let Ok(msg) = Message::load_from_db(t, msg_id).await { - assert_eq!(msg.from_id, ContactId::UNDEFINED); - assert_eq!(msg.to_id, ContactId::UNDEFINED); - assert_eq!(msg.text, ""); - let rawtxt: Option = t - .sql - .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,)) - .await - .unwrap(); - assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}"); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_expired_imap_messages() -> Result<()> { - let t = TestContext::new_alice().await; - const HOUR: i64 = 60 * 60; - let now = time(); - for (id, timestamp, ephemeral_timestamp) in &[ - (900, now - 2 * HOUR, 0), - (1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0), - (1010, now - 23 * HOUR, 0), - (1020, now - 21 * HOUR, 0), - (1030, now - 19 * HOUR, 0), - (2000, now - 18 * HOUR, now - HOUR), - (2020, now - 17 * HOUR, now + HOUR), - (3000, now + HOUR, 0), - ] { - let message_id = id.to_string(); - t.sql - .execute( - "INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);", - (id, &message_id, timestamp, ephemeral_timestamp), - ) - .await?; - t.sql - .execute( - "INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');", - (&message_id, id), - ) - .await?; - } - - async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> { - assert_eq!( - context - .sql - .count( - "SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?", - (id.to_string(),), - ) - .await?, - 1 - ); - Ok(()) - } - - async fn remove_uid(context: &Context, id: u32) -> Result<()> { - context - .sql - .execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),)) - .await?; - Ok(()) - } - - // This should mark message 2000 for deletion. - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 2000).await?; - remove_uid(&t, 2000).await?; - // No other messages are marked for deletion. - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM imap WHERE target=''", ()) - .await?, - 0 - ); - - t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1000).await?; - - MsgId::new(1000) - .update_download_state(&t, DownloadState::Available) - .await?; - t.sql - .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ()) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway. - remove_uid(&t, 1000).await?; - - t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) - .await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 1010).await?; - t.sql - .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ()) - .await?; - - MsgId::new(1010) - .update_download_state(&t, DownloadState::Available) - .await?; - delete_expired_imap_messages(&t).await?; - // Keep downloadable for now. - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM imap WHERE target=''", ()) - .await?, - 0 - ); - - t.set_config(Config::DeleteServerAfter, Some("1")).await?; - delete_expired_imap_messages(&t).await?; - test_marked_for_deletion(&t, 3000).await?; - - Ok(()) - } - - // Regression test for a bug in the timer rollback protection. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_timer_references() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Message with Message-ID and no timer is received. - receive_imf( - &alice, - b"From: Bob \n\ - To: Alice \n\ - Chat-Version: 1.0\n\ - Subject: Subject\n\ - Message-ID: \n\ - Date: Sun, 22 Mar 2020 00:10:00 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let msg = alice.get_last_msg().await; - let chat_id = msg.chat_id; - assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); - - // Message with Message-ID is received. - receive_imf( - &alice, - b"From: Bob \n\ - To: Alice \n\ - Chat-Version: 1.0\n\ - Subject: Subject\n\ - Message-ID: \n\ - Date: Sun, 22 Mar 2020 00:11:00 +0000\n\ - Ephemeral-Timer: 60\n\ - \n\ - second message\n", - false, - ) - .await?; - assert_eq!( - chat_id.get_ephemeral_timer(&alice).await?, - Timer::Enabled { duration: 60 } - ); - let msg = alice.get_last_msg().await; - - // Message is deleted when its timer expires. - msg.id.trash(&alice, false).await?; - - // Message with Message-ID , referencing and - // , is received. The message is not in the - // database anymore, so the timer should be applied unconditionally without rollback - // protection. - // - // Previously Delta Chat fallen back to using in this case and - // compared received timer value to the timer value of the . Because - // their timer values are the same ("disabled"), Delta Chat assumed that the timer was not - // changed explicitly and the change should be ignored. - // - // The message also contains a quote of the first message to test that only References: - // header and not In-Reply-To: is consulted by the rollback protection. - receive_imf( - &alice, - b"From: Bob \n\ - To: Alice \n\ - Chat-Version: 1.0\n\ - Subject: Subject\n\ - Message-ID: \n\ - Date: Sun, 22 Mar 2020 00:12:00 +0000\n\ - References: \n\ - In-Reply-To: \n\ - \n\ - > hello\n", - false, - ) - .await?; - - let msg = alice.get_last_msg().await; - assert_eq!( - msg.chat_id.get_ephemeral_timer(&alice).await?, - Timer::Disabled - ); - - Ok(()) - } - - // Tests that if we are offline for a time longer than the ephemeral timer duration, the message - // is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a - // successful reconnection. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_msg_offline() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.org") - .await; - let duration = 60; - chat.id - .set_ephemeral_timer(&alice, Timer::Enabled { duration }) - .await?; - let mut msg = Message::new_text("hi".to_string()); - assert!(chat::send_msg_sync(&alice, chat.id, &mut msg) - .await - .is_err()); - let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?"; - assert!(alice.sql.exists(stmt, (msg.id,)).await?); - let now = time(); - check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1) - .await?; - assert!(alice.sql.exists(stmt, (msg.id,)).await?); - - Ok(()) - } - - /// Tests that POI location is deleted when ephemeral message expires. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ephemeral_poi_location() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let chat = alice.create_chat(bob).await; - - let duration = 60; - chat.id - .set_ephemeral_timer(alice, Timer::Enabled { duration }) - .await?; - let sent = alice.pop_sent_msg().await; - bob.recv_msg(&sent).await; - - let mut poi_msg = Message::new_text("Here".to_string()); - poi_msg.set_location(10.0, 20.0); - - let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await; - let bob_received_message = bob.recv_msg(&alice_sent_message).await; - markseen_msgs(bob, vec![bob_received_message.id]).await?; - - for account in [alice, bob] { - let locations = location::get_range(account, None, None, 0, 0).await?; - assert_eq!(locations.len(), 1); - } - - SystemTime::shift(Duration::from_secs(100)); - - for account in [alice, bob] { - delete_expired_messages(account, time()).await?; - let locations = location::get_range(account, None, None, 0, 0).await?; - assert_eq!(locations.len(), 0); - } - - Ok(()) - } - - /// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> { - let context = TestContext::new().await; - let chat_id = ChatId::new(12345); - assert!(chat_id.get_ephemeral_timer(&context).await.is_err()); - - Ok(()) - } - - /// Tests that ephemeral timer is started when the chat is noticed. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_noticed_ephemeral_timer() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let chat = alice.create_chat(bob).await; - let duration = 60; - chat.id - .set_ephemeral_timer(alice, Timer::Enabled { duration }) - .await?; - let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await; - - marknoticed_chat(bob, bob_received_message.chat_id).await?; - SystemTime::shift(Duration::from_secs(100)); - - delete_expired_messages(bob, time()).await?; - - assert!(Message::load_from_db_optional(bob, bob_received_message.id) - .await? - .is_none()); - Ok(()) - } - - /// Tests that archiving the chat starts ephemeral timer. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_archived_ephemeral_timer() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let chat = alice.create_chat(bob).await; - let duration = 60; - chat.id - .set_ephemeral_timer(alice, Timer::Enabled { duration }) - .await?; - let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await; - - bob_received_message - .chat_id - .set_visibility(bob, ChatVisibility::Archived) - .await?; - SystemTime::shift(Duration::from_secs(100)); - - delete_expired_messages(bob, time()).await?; - - assert!(Message::load_from_db_optional(bob, bob_received_message.id) - .await? - .is_none()); - - // Bob mutes the chat so it is not unarchived. - set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?; - - // Now test that for already archived chat - // timer is started if all archived chats are marked as noticed. - let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await; - assert_eq!(bob_received_message_2.state, MessageState::InFresh); - - marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?; - SystemTime::shift(Duration::from_secs(100)); - - delete_expired_messages(bob, time()).await?; - - assert!( - Message::load_from_db_optional(bob, bob_received_message_2.id) - .await? - .is_none() - ); - - Ok(()) - } -} +mod ephemeral_tests; diff --git a/src/ephemeral/ephemeral_tests.rs b/src/ephemeral/ephemeral_tests.rs new file mode 100644 index 0000000000..d128e78c7a --- /dev/null +++ b/src/ephemeral/ephemeral_tests.rs @@ -0,0 +1,820 @@ +use super::*; +use crate::chat::{ + add_contact_to_chat, marknoticed_chat, remove_contact_from_chat, set_muted, ChatVisibility, + MuteDuration, +}; +use crate::config::Config; +use crate::constants::DC_CHAT_ID_ARCHIVED_LINK; +use crate::download::DownloadState; +use crate::location; +use crate::message::markseen_msgs; +use crate::receive_imf::receive_imf; +use crate::test_utils::{TestContext, TestContextManager}; +use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE; +use crate::{ + chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus}, + tools::IsNoneOrEmpty, +}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_ephemeral_messages() { + let context = TestContext::new().await; + + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await, + "You disabled message deletion timer." + ); + + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, ContactId::SELF) + .await, + "You set message deletion timer to 1 s." + ); + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, ContactId::SELF) + .await, + "You set message deletion timer to 30 s." + ); + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF) + .await, + "You set message deletion timer to 1 minute." + ); + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF) + .await, + "You set message deletion timer to 1.5 minutes." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 30 * 60 }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 30 minutes." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { duration: 60 * 60 }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 1 hour." + ); + assert_eq!( + stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 5400 }, ContactId::SELF) + .await, + "You set message deletion timer to 1.5 hours." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { + duration: 2 * 60 * 60 + }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 2 hours." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { + duration: 24 * 60 * 60 + }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 1 day." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { + duration: 2 * 24 * 60 * 60 + }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 2 days." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { + duration: 7 * 24 * 60 * 60 + }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 1 week." + ); + assert_eq!( + stock_ephemeral_timer_changed( + &context, + Timer::Enabled { + duration: 4 * 7 * 24 * 60 * 60 + }, + ContactId::SELF + ) + .await, + "You set message deletion timer to 4 weeks." + ); +} + +/// Test enabling and disabling ephemeral timer remotely. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_enable_disable() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let chat_alice = alice.create_chat(&bob).await.id; + let chat_bob = bob.create_chat(&alice).await.id; + + chat_alice + .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 }) + .await?; + let sent = alice.pop_sent_msg().await; + bob.recv_msg(&sent).await; + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Enabled { duration: 60 } + ); + + chat_alice + .set_ephemeral_timer(&alice.ctx, Timer::Disabled) + .await?; + let sent = alice.pop_sent_msg().await; + bob.recv_msg(&sent).await; + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Disabled + ); + + Ok(()) +} + +/// Test that enabling ephemeral timer in unpromoted group does not send a message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_unpromoted() -> Result<()> { + let alice = TestContext::new_alice().await; + + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?; + + // Group is unpromoted, the timer can be changed without sending a message. + assert!(chat_id.is_unpromoted(&alice).await?); + chat_id + .set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 }) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_none()); + assert_eq!( + chat_id.get_ephemeral_timer(&alice).await?, + Timer::Enabled { duration: 60 } + ); + + // Promote the group. + send_text_msg(&alice, chat_id, "hi!".to_string()).await?; + assert!(chat_id.is_promoted(&alice).await?); + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + + chat_id + .set_ephemeral_timer(&alice.ctx, Timer::Disabled) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); + + Ok(()) +} + +/// Test that timer is enabled even if the message explicitly enabling the timer is lost. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_enable_lost() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let chat_alice = alice.create_chat(&bob).await.id; + let chat_bob = bob.create_chat(&alice).await.id; + + // Alice enables the timer. + chat_alice + .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 }) + .await?; + assert_eq!( + chat_alice.get_ephemeral_timer(&alice.ctx).await?, + Timer::Enabled { duration: 60 } + ); + // The message enabling the timer is lost. + let _sent = alice.pop_sent_msg().await; + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Disabled, + ); + + // Alice sends a text message. + let mut msg = Message::new(Viewtype::Text); + chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; + let sent = alice.pop_sent_msg().await; + + // Bob receives text message and enables the timer, even though explicit timer update was + // lost previously. + bob.recv_msg(&sent).await; + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Enabled { duration: 60 } + ); + + Ok(()) +} + +/// Test that Alice replying to the chat without a timer at the same time as Bob enables the +/// timer does not result in disabling the timer on the Bob's side. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_timer_rollback() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + let chat_alice = alice.create_chat(&bob).await.id; + let chat_bob = bob.create_chat(&alice).await.id; + + // Alice sends message to Bob + let mut msg = Message::new(Viewtype::Text); + chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; + let sent = alice.pop_sent_msg().await; + bob.recv_msg(&sent).await; + + // Alice sends second message to Bob, with no timer + let mut msg = Message::new(Viewtype::Text); + chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?; + let sent = alice.pop_sent_msg().await; + + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Disabled + ); + + // Bob sets ephemeral timer and sends a message about timer change + chat_bob + .set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 }) + .await?; + let sent_timer_change = bob.pop_sent_msg().await; + + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Enabled { duration: 60 } + ); + + // Bob receives message from Alice. + // Alice message has no timer. However, Bob should not disable timer, + // because Alice replies to old message. + bob.recv_msg(&sent).await; + + assert_eq!( + chat_alice.get_ephemeral_timer(&alice.ctx).await?, + Timer::Disabled + ); + assert_eq!( + chat_bob.get_ephemeral_timer(&bob.ctx).await?, + Timer::Enabled { duration: 60 } + ); + + // Alice receives message from Bob + alice.recv_msg(&sent_timer_change).await; + + assert_eq!( + chat_alice.get_ephemeral_timer(&alice.ctx).await?, + Timer::Enabled { duration: 60 } + ); + + // Bob disables the chat timer. + // Note that the last message in the Bob's chat is from Alice and has no timer, + // but the chat timer is enabled. + chat_bob + .set_ephemeral_timer(&bob.ctx, Timer::Disabled) + .await?; + alice.recv_msg(&bob.pop_sent_msg().await).await; + assert_eq!( + chat_alice.get_ephemeral_timer(&alice.ctx).await?, + Timer::Disabled + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_delete_msgs() -> Result<()> { + let t = TestContext::new_alice().await; + let self_chat = t.get_self_chat().await; + + assert_eq!(next_expiration_timestamp(&t).await, None); + + t.send_text(self_chat.id, "Saved message, which we delete manually") + .await; + let msg = t.get_last_msg_in(self_chat.id).await; + msg.id.trash(&t, false).await?; + check_msg_is_deleted(&t, &self_chat, msg.id).await; + + self_chat + .id + .set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 }) + .await + .unwrap(); + + // Send a saved message which will be deleted after 3600s + let now = time(); + let msg = t.send_text(self_chat.id, "Message text").await; + + check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601) + .await + .unwrap(); + + // Set DeleteDeviceAfter to 1800s. Then send a saved message which will + // still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages. + t.set_config(Config::DeleteDeviceAfter, Some("1800")) + .await?; + + let now = time(); + let msg = t.send_text(self_chat.id, "Message text").await; + + check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601) + .await + .unwrap(); + + // Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter. + let bob_chat = t.create_chat_with_contact("", "bob@example.net").await; + let now = time(); + let msg = t.send_text(bob_chat.id, "Message text").await; + + check_msg_will_be_deleted( + &t, + msg.sender_msg_id, + &bob_chat, + now + 1799, + // The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and + // therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later. + time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE, + ) + .await + .unwrap(); + + // Enable ephemeral messages with Bob -> message will be deleted after 60s. + // This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time). + bob_chat + .id + .set_ephemeral_timer(&t, Timer::Enabled { duration: 60 }) + .await?; + + let now = time(); + let msg = t.send_text(bob_chat.id, "Message text").await; + + check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61) + .await + .unwrap(); + + Ok(()) +} + +async fn check_msg_will_be_deleted( + t: &TestContext, + msg_id: MsgId, + chat: &Chat, + not_deleted_at: i64, + deleted_at: i64, +) -> Result<()> { + let next_expiration = next_expiration_timestamp(t).await.unwrap(); + + assert!(next_expiration > not_deleted_at); + delete_expired_messages(t, not_deleted_at).await?; + + let loaded = Message::load_from_db(t, msg_id).await?; + assert!(!loaded.text.is_empty()); + assert_eq!(loaded.chat_id, chat.id); + + assert!(next_expiration < deleted_at); + delete_expired_messages(t, deleted_at).await?; + t.evtracker + .get_matching(|evt| { + if let EventType::MsgDeleted { + msg_id: event_msg_id, + .. + } = evt + { + *event_msg_id == msg_id + } else { + false + } + }) + .await; + + let loaded = Message::load_from_db_optional(t, msg_id).await?; + assert!(loaded.is_none()); + + // Check that the msg was deleted locally. + check_msg_is_deleted(t, chat, msg_id).await; + + Ok(()) +} + +async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) { + let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap(); + // Check that the chat is empty except for possibly info messages: + for item in &chat_items { + if let ChatItem::Message { msg_id } = item { + let msg = Message::load_from_db(t, *msg_id).await.unwrap(); + assert!(msg.is_info()) + } + } + + // Check that if there is a message left, the text and metadata are gone + if let Ok(msg) = Message::load_from_db(t, msg_id).await { + assert_eq!(msg.from_id, ContactId::UNDEFINED); + assert_eq!(msg.to_id, ContactId::UNDEFINED); + assert_eq!(msg.text, ""); + let rawtxt: Option = t + .sql + .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,)) + .await + .unwrap(); + assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}"); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_expired_imap_messages() -> Result<()> { + let t = TestContext::new_alice().await; + const HOUR: i64 = 60 * 60; + let now = time(); + for (id, timestamp, ephemeral_timestamp) in &[ + (900, now - 2 * HOUR, 0), + (1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0), + (1010, now - 23 * HOUR, 0), + (1020, now - 21 * HOUR, 0), + (1030, now - 19 * HOUR, 0), + (2000, now - 18 * HOUR, now - HOUR), + (2020, now - 17 * HOUR, now + HOUR), + (3000, now + HOUR, 0), + ] { + let message_id = id.to_string(); + t.sql + .execute( + "INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);", + (id, &message_id, timestamp, ephemeral_timestamp), + ) + .await?; + t.sql + .execute( + "INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');", + (&message_id, id), + ) + .await?; + } + + async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> { + assert_eq!( + context + .sql + .count( + "SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?", + (id.to_string(),), + ) + .await?, + 1 + ); + Ok(()) + } + + async fn remove_uid(context: &Context, id: u32) -> Result<()> { + context + .sql + .execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),)) + .await?; + Ok(()) + } + + // This should mark message 2000 for deletion. + delete_expired_imap_messages(&t).await?; + test_marked_for_deletion(&t, 2000).await?; + remove_uid(&t, 2000).await?; + // No other messages are marked for deletion. + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM imap WHERE target=''", ()) + .await?, + 0 + ); + + t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) + .await?; + delete_expired_imap_messages(&t).await?; + test_marked_for_deletion(&t, 1000).await?; + + MsgId::new(1000) + .update_download_state(&t, DownloadState::Available) + .await?; + t.sql + .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ()) + .await?; + delete_expired_imap_messages(&t).await?; + test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway. + remove_uid(&t, 1000).await?; + + t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) + .await?; + delete_expired_imap_messages(&t).await?; + test_marked_for_deletion(&t, 1010).await?; + t.sql + .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ()) + .await?; + + MsgId::new(1010) + .update_download_state(&t, DownloadState::Available) + .await?; + delete_expired_imap_messages(&t).await?; + // Keep downloadable for now. + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM imap WHERE target=''", ()) + .await?, + 0 + ); + + t.set_config(Config::DeleteServerAfter, Some("1")).await?; + delete_expired_imap_messages(&t).await?; + test_marked_for_deletion(&t, 3000).await?; + + Ok(()) +} + +// Regression test for a bug in the timer rollback protection. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_timer_references() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Message with Message-ID and no timer is received. + receive_imf( + &alice, + b"From: Bob \n\ + To: Alice \n\ + Chat-Version: 1.0\n\ + Subject: Subject\n\ + Message-ID: \n\ + Date: Sun, 22 Mar 2020 00:10:00 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let msg = alice.get_last_msg().await; + let chat_id = msg.chat_id; + assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); + + // Message with Message-ID is received. + receive_imf( + &alice, + b"From: Bob \n\ + To: Alice \n\ + Chat-Version: 1.0\n\ + Subject: Subject\n\ + Message-ID: \n\ + Date: Sun, 22 Mar 2020 00:11:00 +0000\n\ + Ephemeral-Timer: 60\n\ + \n\ + second message\n", + false, + ) + .await?; + assert_eq!( + chat_id.get_ephemeral_timer(&alice).await?, + Timer::Enabled { duration: 60 } + ); + let msg = alice.get_last_msg().await; + + // Message is deleted when its timer expires. + msg.id.trash(&alice, false).await?; + + // Message with Message-ID , referencing and + // , is received. The message is not in the + // database anymore, so the timer should be applied unconditionally without rollback + // protection. + // + // Previously Delta Chat fallen back to using in this case and + // compared received timer value to the timer value of the . Because + // their timer values are the same ("disabled"), Delta Chat assumed that the timer was not + // changed explicitly and the change should be ignored. + // + // The message also contains a quote of the first message to test that only References: + // header and not In-Reply-To: is consulted by the rollback protection. + receive_imf( + &alice, + b"From: Bob \n\ + To: Alice \n\ + Chat-Version: 1.0\n\ + Subject: Subject\n\ + Message-ID: \n\ + Date: Sun, 22 Mar 2020 00:12:00 +0000\n\ + References: \n\ + In-Reply-To: \n\ + \n\ + > hello\n", + false, + ) + .await?; + + let msg = alice.get_last_msg().await; + assert_eq!( + msg.chat_id.get_ephemeral_timer(&alice).await?, + Timer::Disabled + ); + + Ok(()) +} + +// Tests that if we are offline for a time longer than the ephemeral timer duration, the message +// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a +// successful reconnection. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_msg_offline() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat = alice + .create_chat_with_contact("Bob", "bob@example.org") + .await; + let duration = 60; + chat.id + .set_ephemeral_timer(&alice, Timer::Enabled { duration }) + .await?; + let mut msg = Message::new_text("hi".to_string()); + assert!(chat::send_msg_sync(&alice, chat.id, &mut msg) + .await + .is_err()); + let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?"; + assert!(alice.sql.exists(stmt, (msg.id,)).await?); + let now = time(); + check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?; + assert!(alice.sql.exists(stmt, (msg.id,)).await?); + + Ok(()) +} + +/// Tests that POI location is deleted when ephemeral message expires. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_poi_location() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let chat = alice.create_chat(bob).await; + + let duration = 60; + chat.id + .set_ephemeral_timer(alice, Timer::Enabled { duration }) + .await?; + let sent = alice.pop_sent_msg().await; + bob.recv_msg(&sent).await; + + let mut poi_msg = Message::new_text("Here".to_string()); + poi_msg.set_location(10.0, 20.0); + + let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await; + let bob_received_message = bob.recv_msg(&alice_sent_message).await; + markseen_msgs(bob, vec![bob_received_message.id]).await?; + + for account in [alice, bob] { + let locations = location::get_range(account, None, None, 0, 0).await?; + assert_eq!(locations.len(), 1); + } + + SystemTime::shift(Duration::from_secs(100)); + + for account in [alice, bob] { + delete_expired_messages(account, time()).await?; + let locations = location::get_range(account, None, None, 0, 0).await?; + assert_eq!(locations.len(), 0); + } + + Ok(()) +} + +/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> { + let context = TestContext::new().await; + let chat_id = ChatId::new(12345); + assert!(chat_id.get_ephemeral_timer(&context).await.is_err()); + + Ok(()) +} + +/// Tests that ephemeral timer is started when the chat is noticed. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_noticed_ephemeral_timer() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let chat = alice.create_chat(bob).await; + let duration = 60; + chat.id + .set_ephemeral_timer(alice, Timer::Enabled { duration }) + .await?; + let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await; + + marknoticed_chat(bob, bob_received_message.chat_id).await?; + SystemTime::shift(Duration::from_secs(100)); + + delete_expired_messages(bob, time()).await?; + + assert!(Message::load_from_db_optional(bob, bob_received_message.id) + .await? + .is_none()); + Ok(()) +} + +/// Tests that archiving the chat starts ephemeral timer. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_archived_ephemeral_timer() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let chat = alice.create_chat(bob).await; + let duration = 60; + chat.id + .set_ephemeral_timer(alice, Timer::Enabled { duration }) + .await?; + let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await; + + bob_received_message + .chat_id + .set_visibility(bob, ChatVisibility::Archived) + .await?; + SystemTime::shift(Duration::from_secs(100)); + + delete_expired_messages(bob, time()).await?; + + assert!(Message::load_from_db_optional(bob, bob_received_message.id) + .await? + .is_none()); + + // Bob mutes the chat so it is not unarchived. + set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?; + + // Now test that for already archived chat + // timer is started if all archived chats are marked as noticed. + let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await; + assert_eq!(bob_received_message_2.state, MessageState::InFresh); + + marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?; + SystemTime::shift(Duration::from_secs(100)); + + delete_expired_messages(bob, time()).await?; + + assert!( + Message::load_from_db_optional(bob, bob_received_message_2.id) + .await? + .is_none() + ); + + Ok(()) +} + +/// Tests that non-members cannot change ephemeral timer settings. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ephemeral_timer_non_member() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await; + let alice_chat_id = + create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?; + add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?; + + let sent = alice.pop_sent_msg().await; + let bob_chat_id = bob.recv_msg(&sent).await.chat_id; + + // Bob wants to modify the timer. + bob_chat_id.accept(bob).await?; + bob_chat_id + .set_ephemeral_timer(bob, Timer::Enabled { duration: 60 }) + .await?; + let sent_ephemeral_timer_change = bob.pop_sent_msg().await; + + // Alice removes Bob before receiving the timer change. + remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?; + alice.recv_msg(&sent_ephemeral_timer_change).await; + + // Timer is not changed because Bob is not a member. + assert_eq!( + alice_chat_id.get_ephemeral_timer(alice).await?, + Timer::Disabled + ); + + Ok(()) +} diff --git a/src/events/payload.rs b/src/events/payload.rs index 302355ad63..a5e2f99653 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -95,7 +95,10 @@ pub enum EventType { contact_id: ContactId, }, - /// Reactions for the message changed. + /// A reaction to one's own sent message received. + /// Typically, the UI will show a notification for that. + /// + /// In addition to this event, ReactionsChanged is emitted. IncomingReaction { /// ID of the chat which the message belongs to. chat_id: ChatId, @@ -216,6 +219,12 @@ pub enum EventType { timer: EphemeralTimer, }, + /// Chat was deleted. + ChatDeleted { + /// Chat ID. + chat_id: ChatId, + }, + /// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status. /// /// @param data1 (int) If set, this is the contact_id of an added contact that should be selected. diff --git a/src/fuzzing.rs b/src/fuzzing.rs deleted file mode 100644 index c19ae36d16..0000000000 --- a/src/fuzzing.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! # Fuzzing module. -//! -//! This module exposes private APIs for fuzzing. - -/// Fuzzing target for simplify(). -/// -/// Calls simplify() and panics if simplify() panics. -/// Does not return any value to avoid exposing internal crate types. -#[cfg(fuzzing)] -pub fn simplify(input: String, is_chat_message: bool) { - crate::simplify::simplify(input, is_chat_message); -} diff --git a/src/headerdef.rs b/src/headerdef.rs index 1434b53232..cc49fa9a28 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -57,6 +57,7 @@ pub enum HeaderDef { ChatGroupId, ChatGroupName, ChatGroupNameChanged, + ChatGroupNameTimestamp, ChatVerified, ChatGroupAvatar, ChatUserAvatar, @@ -80,6 +81,12 @@ pub enum HeaderDef { ChatDispositionNotificationTo, ChatWebrtcRoom, + /// This message deletes the messages listed in the value by rfc724_mid. + ChatDelete, + + /// This message obsoletes the text of the message defined here by rfc724_mid. + ChatEdit, + /// [Autocrypt](https://autocrypt.org/) header. Autocrypt, AutocryptGossip, diff --git a/src/html.rs b/src/html.rs index 1e5e5097c9..5ba94ee6cc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -11,9 +11,8 @@ use std::mem; use anyhow::{Context as _, Result}; use base64::Engine as _; -use lettre_email::mime::Mime; -use lettre_email::PartBuilder; use mailparse::ParsedContentType; +use mime::Mime; use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -277,16 +276,6 @@ impl MsgId { } } -/// Wraps HTML text into a new text/html mimepart structure. -/// -/// Used on forwarding messages to avoid leaking the original mime structure -/// and also to avoid sending too much, maybe large data. -pub fn new_html_mimepart(html: String) -> PartBuilder { - PartBuilder::new() - .content_type(&"text/html; charset=utf-8".parse::().unwrap()) - .body(html) -} - #[cfg(test)] mod tests { use super::*; @@ -296,7 +285,7 @@ mod tests { use crate::contact::ContactId; use crate::message::{MessengerMessage, Viewtype}; use crate::receive_imf::receive_imf; - use crate::test_utils::TestContext; + use crate::test_utils::{TestContext, TestContextManager}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_htmlparse_plain_unspecified() { @@ -371,7 +360,6 @@ and will be wrapped as usual.
mime-modified should not be set set as there is no html and no special stuff;
although not being a delta-message.
test some special html-characters as < > and & but also " and ' :)
-
"# ); @@ -405,7 +393,6 @@ test some special html-characters as < > and & but also " and &#x r##"

mime-modified set; simplify is always regarded as lossy.

- "## ); } @@ -422,7 +409,6 @@ test some special html-characters as < > and & but also " and &#x this is html

- "## ); } @@ -456,24 +442,25 @@ test some special html-characters as < > and & but also " and &#x #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_html_forwarding() { // alice receives a non-delta html-message - let alice = TestContext::new_alice().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; let chat = alice .create_chat_with_contact("", "sender@testrun.org") .await; let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); - receive_imf(&alice, raw, false).await.unwrap(); + receive_imf(alice, raw, false).await.unwrap(); let msg = alice.get_last_msg_in(chat.get_id()).await; assert_ne!(msg.get_from_id(), ContactId::SELF); assert_eq!(msg.is_dc_message, MessengerMessage::No); assert!(!msg.is_forwarded()); assert!(msg.get_text().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); + let html = msg.get_id().get_html(alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); // alice: create chat with bob and forward received html-message there let chat = alice.create_chat_with_contact("", "bob@example.net").await; - forward_msgs(&alice, &[msg.get_id()], chat.get_id()) + forward_msgs(alice, &[msg.get_id()], chat.get_id()) .await .unwrap(); let msg = alice.get_last_msg_in(chat.get_id()).await; @@ -482,11 +469,11 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); + let html = msg.get_id().get_html(alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); // bob: check that bob also got the html-part of the forwarded message - let bob = TestContext::new_bob().await; + let bob = &tcm.bob().await; let chat = bob.create_chat_with_contact("", "alice@example.org").await; let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; assert_eq!(chat.id, msg.chat_id); @@ -495,7 +482,7 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&bob).await.unwrap().unwrap(); + let html = msg.get_id().get_html(bob).await.unwrap().unwrap(); assert!(html.contains("this is html")); } @@ -533,10 +520,11 @@ test some special html-characters as < > and & but also " and &#x #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_html_forwarding_encrypted() { + let mut tcm = TestContextManager::new(); // Alice receives a non-delta html-message // (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known // contacts, the contact is marked as known by creating a chat using `chat_with_contact()`) - let alice = TestContext::new_alice().await; + let alice = &tcm.alice().await; alice .set_config(Config::ShowEmails, Some("1")) .await @@ -545,19 +533,19 @@ test some special html-characters as < > and & but also " and &#x .create_chat_with_contact("", "sender@testrun.org") .await; let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); - receive_imf(&alice, raw, false).await.unwrap(); + receive_imf(alice, raw, false).await.unwrap(); let msg = alice.get_last_msg_in(chat.get_id()).await; // forward the message to saved-messages, // this will encrypt the message as new_alice() has set up keys let chat = alice.get_self_chat().await; - forward_msgs(&alice, &[msg.get_id()], chat.get_id()) + forward_msgs(alice, &[msg.get_id()], chat.get_id()) .await .unwrap(); let msg = alice.pop_sent_msg().await; // receive the message on another device - let alice = TestContext::new_alice().await; + let alice = &tcm.alice().await; alice .set_config(Config::ShowEmails, Some("0")) .await @@ -570,38 +558,39 @@ test some special html-characters as < > and & but also " and &#x assert!(msg.is_forwarded()); assert!(msg.get_text().contains("this is plain")); assert!(msg.has_html()); - let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); + let html = msg.get_id().get_html(alice).await.unwrap().unwrap(); assert!(html.contains("this is html")); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_set_html() { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; // alice sends a message with html-part to bob - let chat_id = alice.create_chat(&bob).await.id; + let chat_id = alice.create_chat(bob).await.id; let mut msg = Message::new_text("plain text".to_string()); msg.set_html(Some("html text".to_string())); assert!(msg.mime_modified); - chat::send_msg(&alice, chat_id, &mut msg).await.unwrap(); + chat::send_msg(alice, chat_id, &mut msg).await.unwrap(); // check the message is written correctly to alice's db let msg = alice.get_last_msg_in(chat_id).await; assert_eq!(msg.get_text(), "plain text"); assert!(!msg.is_forwarded()); assert!(msg.mime_modified); - let html = msg.get_id().get_html(&alice).await.unwrap().unwrap(); + let html = msg.get_id().get_html(alice).await.unwrap().unwrap(); assert!(html.contains("html text")); // let bob receive the message - let chat_id = bob.create_chat(&alice).await.id; + let chat_id = bob.create_chat(alice).await.id; let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; assert_eq!(msg.chat_id, chat_id); assert_eq!(msg.get_text(), "plain text"); assert!(!msg.is_forwarded()); assert!(msg.mime_modified); - let html = msg.get_id().get_html(&bob).await.unwrap().unwrap(); + let html = msg.get_id().get_html(bob).await.unwrap().unwrap(); assert!(html.contains("html text")); } diff --git a/src/imap.rs b/src/imap.rs index aac2bb944c..dfc53525c2 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -271,21 +271,21 @@ impl Imap { let param = ConfiguredLoginParam::load(context) .await? .context("Not configured")?; + let proxy_config = ProxyConfig::load(context).await?; + let strict_tls = param.strict_tls(proxy_config.is_some()); let imap = Self::new( param.imap.clone(), param.imap_password.clone(), - param.proxy_config.clone(), + proxy_config, ¶m.addr, - param.strict_tls(), + strict_tls, param.oauth2, idle_interrupt_receiver, ); Ok(imap) } - /// Connects or reconnects if needed. - /// - /// It is safe to call this function if already connected, actions are performed only as needed. + /// Connects to IMAP server and returns a new IMAP session. /// /// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`] /// instead if you are going to actually use connection rather than trying connection @@ -324,7 +324,7 @@ impl Imap { } } - info!(context, "Connecting to IMAP server"); + info!(context, "Connecting to IMAP server."); self.connectivity.set_connecting(context).await; self.conn_last_try = tools::Time::now(); @@ -348,10 +348,11 @@ impl Imap { connection_candidate, ) .await + .context("IMAP failed to connect") { Ok(client) => client, Err(err) => { - warn!(context, "IMAP failed to connect: {err:#}."); + warn!(context, "{err:#}."); first_error.get_or_insert(err); continue; } @@ -408,7 +409,7 @@ impl Imap { lp.user ))); self.connectivity.set_preparing(context).await; - info!(context, "Successfully logged into IMAP server"); + info!(context, "Successfully logged into IMAP server."); return Ok(session); } @@ -456,10 +457,10 @@ impl Imap { Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided"))) } - /// Prepare for IMAP operation. + /// Prepare a new IMAP session. /// - /// Ensure that IMAP client is connected, folders are created and IMAP capabilities are - /// determined. + /// This creates a new IMAP connection and ensures + /// that folders are created and IMAP capabilities are determined. pub(crate) async fn prepare(&mut self, context: &Context) -> Result { let configuring = false; let mut session = match self.connect(context, configuring).await { @@ -504,7 +505,7 @@ impl Imap { } let msgs_fetched = self - .fetch_new_messages(context, session, watch_folder, folder_meaning, false) + .fetch_new_messages(context, session, watch_folder, folder_meaning) .await .context("fetch_new_messages")?; if msgs_fetched && context.get_config_delete_device_after().await?.is_some() { @@ -532,7 +533,6 @@ impl Imap { session: &mut Session, folder: &str, folder_meaning: FolderMeaning, - fetch_existing_msgs: bool, ) -> Result { if should_ignore_folder(context, folder, folder_meaning).await? { info!(context, "Not fetching from {folder:?}."); @@ -549,7 +549,7 @@ impl Imap { return Ok(false); } - if !session.new_mail && !fetch_existing_msgs { + if !session.new_mail { info!(context, "No new emails in folder {folder:?}."); return Ok(false); } @@ -558,14 +558,7 @@ impl Imap { let uid_validity = get_uidvalidity(context, folder).await?; let old_uid_next = get_uid_next(context, folder).await?; - let msgs = if fetch_existing_msgs { - session - .prefetch_existing_msgs() - .await - .context("prefetch_existing_msgs")? - } else { - session.prefetch(old_uid_next).await.context("prefetch")? - }; + let msgs = session.prefetch(old_uid_next).await.context("prefetch")?; let read_cnt = msgs.len(); let download_limit = context.download_limit().await?; @@ -718,7 +711,6 @@ impl Imap { uids_fetch_in_batch.split_off(0), &uid_message_ids, fetch_partially, - fetch_existing_msgs, ) .await .context("fetch_many_msgs")?; @@ -783,28 +775,6 @@ impl Imap { .await .context("failed to get recipients from the inbox")?; - if context.get_config_bool(Config::FetchExistingMsgs).await? { - for meaning in [ - FolderMeaning::Mvbox, - FolderMeaning::Inbox, - FolderMeaning::Sent, - ] { - let config = match meaning.to_config() { - Some(c) => c, - None => continue, - }; - if let Some(folder) = context.get_config(config).await? { - info!( - context, - "Fetching existing messages from folder {folder:?}." - ); - self.fetch_new_messages(context, session, &folder, meaning, true) - .await - .context("could not fetch existing messages")?; - } - } - } - info!(context, "Done fetching existing messages."); Ok(()) } @@ -1076,7 +1046,7 @@ impl Session { // Expunge folder if needed, e.g. if some jobs have // deleted messages on the server. if let Err(err) = self.maybe_close_folder(context).await { - warn!(context, "failed to close folder: {:?}", err); + warn!(context, "Failed to close folder: {err:#}."); } Ok(()) @@ -1331,7 +1301,6 @@ impl Session { /// Returns the last UID fetched successfully and the info about each downloaded message. /// If the message is incorrect or there is a failure to write a message to the database, /// it is skipped and the error is logged. - #[expect(clippy::too_many_arguments)] pub(crate) async fn fetch_many_msgs( &mut self, context: &Context, @@ -1340,7 +1309,6 @@ impl Session { request_uids: Vec, uid_message_ids: &BTreeMap, fetch_partially: bool, - fetching_existing_messages: bool, ) -> Result<(Option, Vec)> { let mut last_uid = None; let mut received_msgs = Vec::new(); @@ -1474,7 +1442,6 @@ impl Session { body, is_seen, partial, - fetching_existing_messages, ) .await { @@ -1589,15 +1556,15 @@ impl Session { }; if self.can_metadata() && self.can_push() { - let device_token_changed = - context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token); + let old_encrypted_device_token = + context.get_config(Config::EncryptedDeviceToken).await?; - if device_token_changed { - let folder = context - .get_config(Config::ConfiguredInboxFolder) - .await? - .context("INBOX is not configured")?; + // Whether we need to update encrypted device token. + let device_token_changed = old_encrypted_device_token.is_none() + || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token); + let new_encrypted_device_token; + if device_token_changed { let encrypted_device_token = encrypt_device_token(&device_token) .context("Failed to encrypt device token")?; @@ -1606,22 +1573,23 @@ impl Session { // . let encrypted_device_token_len = encrypted_device_token.len(); - if encrypted_device_token_len <= 4096 { - self.run_command_and_check_ok(&format_setmetadata( - &folder, - &encrypted_device_token, - )) - .await - .context("SETMETADATA command failed")?; + // Store device token saved on the server + // to prevent storing duplicate tokens. + // The server cannot deduplicate on its own + // because encryption gives a different + // result each time. + context + .set_config_internal(Config::DeviceToken, Some(&device_token)) + .await?; + context + .set_config_internal( + Config::EncryptedDeviceToken, + Some(&encrypted_device_token), + ) + .await?; - // Store device token saved on the server - // to prevent storing duplicate tokens. - // The server cannot deduplicate on its own - // because encryption gives a different - // result each time. - context - .set_config_internal(Config::DeviceToken, Some(&device_token)) - .await?; + if encrypted_device_token_len <= 4096 { + new_encrypted_device_token = Some(encrypted_device_token); } else { // If Apple or Google (FCM) gives us a very large token, // do not even try to give it to IMAP servers. @@ -1633,9 +1601,29 @@ impl Session { // of any length, but there is no reason for tokens // to be that large even after OpenPGP encryption. warn!(context, "Device token is too long for LITERAL-, ignoring."); + new_encrypted_device_token = None; } + } else { + new_encrypted_device_token = old_encrypted_device_token; + } + + // Store new encrypted device token on the server + // even if it is the same as the old one. + if let Some(encrypted_device_token) = new_encrypted_device_token { + let folder = context + .get_config(Config::ConfiguredInboxFolder) + .await? + .context("INBOX is not configured")?; + + self.run_command_and_check_ok(&format_setmetadata( + &folder, + &encrypted_device_token, + )) + .await + .context("SETMETADATA command failed")?; + + context.push_subscribed.store(true, Ordering::Relaxed); } - context.push_subscribed.store(true, Ordering::Relaxed); } else if !context.push_subscriber.heartbeat_subscribed().await { let context = context.clone(); // Subscribe for heartbeat notifications. @@ -2620,342 +2608,4 @@ async fn add_all_recipients_as_contacts( } #[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::TestContext; - - #[test] - fn test_get_folder_meaning_by_name() { - assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent); - assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent); - assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent); - assert_eq!( - get_folder_meaning_by_name("Messages envoyés"), - FolderMeaning::Sent - ); - assert_eq!( - get_folder_meaning_by_name("mEsSaGes envoyÉs"), - FolderMeaning::Sent - ); - assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown); - assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam); - assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_uid_next_validity() { - let t = TestContext::new_alice().await; - assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0); - assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0); - - set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap(); - assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7); - assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0); - - set_uid_next(&t.ctx, "Inbox", 5).await.unwrap(); - set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap(); - assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5); - assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6); - } - - #[test] - fn test_build_sequence_sets() { - assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]); - - let cases = vec![ - (vec![1], "1"), - (vec![3291], "3291"), - (vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"), - (vec![1, 2, 3], "1:3"), - (vec![1, 4, 5, 6], "1,4:6"), - ((1..=500).collect(), "1:500"), - (vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"), - ]; - for (input, s) in cases { - assert_eq!( - build_sequence_sets(&input).unwrap(), - vec![(input, s.into())] - ); - } - - let has_number = |(uids, s): &(Vec, String), number| { - uids.iter().any(|&n| n == number) - && s.split(',').any(|n| n.parse::().unwrap() == number) - }; - - let numbers: Vec<_> = (2..=500).step_by(2).collect(); - let result = build_sequence_sets(&numbers).unwrap(); - for (_, set) in &result { - assert!(set.len() < 1010); - assert!(!set.ends_with(',')); - assert!(!set.starts_with(',')); - } - assert!(result.len() == 1); // these UIDs fit in one set - for &number in &numbers { - assert!(result.iter().any(|r| has_number(r, number))); - } - - let numbers: Vec<_> = (1..=1000).step_by(3).collect(); - let result = build_sequence_sets(&numbers).unwrap(); - for (_, set) in &result { - assert!(set.len() < 1010); - assert!(!set.ends_with(',')); - assert!(!set.starts_with(',')); - } - let (last_uids, last_str) = result.last().unwrap(); - assert_eq!( - last_uids.get((last_uids.len() - 2)..).unwrap(), - &[997, 1000] - ); - assert!(last_str.ends_with("997,1000")); - assert!(result.len() == 2); // This time we need 2 sets - for &number in &numbers { - assert!(result.iter().any(|r| has_number(r, number))); - } - - let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect(); - let result = build_sequence_sets(&numbers).unwrap(); - for (_, set) in &result { - assert!(set.len() < 1010); - assert!(!set.ends_with(',')); - assert!(!set.starts_with(',')); - } - assert_eq!(result.len(), 6); - for &number in &numbers { - assert!(result.iter().any(|r| has_number(r, number))); - } - } - - async fn check_target_folder_combination( - folder: &str, - mvbox_move: bool, - chat_msg: bool, - expected_destination: &str, - accepted_chat: bool, - outgoing: bool, - setupmessage: bool, - ) -> Result<()> { - println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"); - - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat")) - .await?; - t.ctx - .set_config(Config::ConfiguredSentboxFolder, Some("Sent")) - .await?; - t.ctx - .set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" })) - .await?; - - if accepted_chat { - let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?; - ChatId::create_for_contact(&t.ctx, contact_id).await?; - } - let temp; - - let bytes = if setupmessage { - include_bytes!("../test-data/message/AutocryptSetupMessage.eml") - } else { - temp = format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - {}\ - Subject: foo\n\ - Message-ID: \n\ - {}\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - if outgoing { - "From: alice@example.org\nTo: bob@example.net\n" - } else { - "From: bob@example.net\nTo: alice@example.org\n" - }, - if chat_msg { "Chat-Version: 1.0\n" } else { "" }, - ); - temp.as_bytes() - }; - - let (headers, _) = mailparse::parse_headers(bytes)?; - let actual = if let Some(config) = - target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await? - { - t.get_config(config).await? - } else { - None - }; - - let expected = if expected_destination == folder { - None - } else { - Some(expected_destination) - }; - assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"); - Ok(()) - } - - // chat_msg means that the message was sent by Delta Chat - // The tuples are (folder, mvbox_move, chat_msg, expected_destination) - const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[ - ("INBOX", false, false, "INBOX"), - ("INBOX", false, true, "INBOX"), - ("INBOX", true, false, "INBOX"), - ("INBOX", true, true, "DeltaChat"), - ("Sent", false, false, "Sent"), - ("Sent", false, true, "Sent"), - ("Sent", true, false, "Sent"), - ("Sent", true, true, "DeltaChat"), - ("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs - ("Spam", false, true, "INBOX"), - ("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs - ("Spam", true, true, "DeltaChat"), - ]; - - // These are the same as above, but non-chat messages in Spam stay in Spam - const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[ - ("INBOX", false, false, "INBOX"), - ("INBOX", false, true, "INBOX"), - ("INBOX", true, false, "INBOX"), - ("INBOX", true, true, "DeltaChat"), - ("Sent", false, false, "Sent"), - ("Sent", false, true, "Sent"), - ("Sent", true, false, "Sent"), - ("Sent", true, true, "DeltaChat"), - ("Spam", false, false, "Spam"), - ("Spam", false, true, "INBOX"), - ("Spam", true, false, "Spam"), - ("Spam", true, true, "DeltaChat"), - ]; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_target_folder_incoming_accepted() -> Result<()> { - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { - check_target_folder_combination( - folder, - *mvbox_move, - *chat_msg, - expected_destination, - true, - false, - false, - ) - .await?; - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_target_folder_incoming_request() -> Result<()> { - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST { - check_target_folder_combination( - folder, - *mvbox_move, - *chat_msg, - expected_destination, - false, - false, - false, - ) - .await?; - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_target_folder_outgoing() -> Result<()> { - // Test outgoing emails - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { - check_target_folder_combination( - folder, - *mvbox_move, - *chat_msg, - expected_destination, - true, - true, - false, - ) - .await?; - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_target_folder_setupmsg() -> Result<()> { - // Test setupmessages - for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT { - check_target_folder_combination( - folder, - *mvbox_move, - *chat_msg, - if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam" - false, - true, - true, - ) - .await?; - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_imap_search_command() -> Result<()> { - let t = TestContext::new_alice().await; - assert_eq!( - get_imap_self_sent_search_command(&t.ctx).await?, - r#"FROM "alice@example.org""# - ); - - t.ctx.set_primary_self_addr("alice@another.com").await?; - assert_eq!( - get_imap_self_sent_search_command(&t.ctx).await?, - r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"# - ); - - t.ctx.set_primary_self_addr("alice@third.com").await?; - assert_eq!( - get_imap_self_sent_search_command(&t.ctx).await?, - r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"# - ); - - Ok(()) - } - - #[test] - fn test_uid_grouper() { - // Input: sequence of (rowid: i64, uid: u32, target: String) - // Output: sequence of (target: String, rowid_set: Vec, uid_set: String) - let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]); - let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); - assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]); - - let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]); - let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); - assert_eq!( - res, - vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())] - ); - - let grouper = UidGrouper::from([ - (1, 2, "INBOX".to_string()), - (2, 2, "INBOX".to_string()), - (3, 3, "INBOX".to_string()), - ]); - let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); - assert_eq!( - res, - vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())] - ); - } - - #[test] - fn test_setmetadata_device_token() { - assert_eq!( - format_setmetadata("INBOX", "foobarbaz"), - "SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)" - ); - assert_eq!( - format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"), - "SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)" - ); - } -} +mod imap_tests; diff --git a/src/imap/idle.rs b/src/imap/idle.rs index 346d857403..940990483e 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -3,7 +3,6 @@ use std::time::Duration; use anyhow::{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; @@ -27,8 +26,6 @@ impl Session { idle_interrupt_receiver: Receiver<()>, folder: &str, ) -> Result { - use futures::future::FutureExt; - let create = true; self.select_with_uidvalidity(context, folder, create) .await?; @@ -63,42 +60,46 @@ impl Session { handle.as_mut().set_read_timeout(None); let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT); - enum Event { - IdleResponse(IdleResponse), - Interrupt, - } - info!( context, "IDLE entering wait-on-remote state in folder {folder:?}." ); - let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async { - idle_interrupt_receiver.recv().await.ok(); - // cancel imap idle connection properly - drop(interrupt); + // Spawn a task to relay interrupts from `idle_interrupt_receiver` + // into interruptions of IDLE. + let interrupt_relay = { + let context = context.clone(); + let folder = folder.to_string(); + + tokio::spawn(async move { + idle_interrupt_receiver.recv().await.ok(); - Ok(Event::Interrupt) - }); + info!(context, "{folder:?}: Received interrupt, stopping IDLE."); - match fut.await { - Ok(Event::IdleResponse(IdleResponse::NewData(x))) => { + // Drop `interrupt` in order to stop the IMAP IDLE. + drop(interrupt); + }) + }; + + match idle_wait.await { + Ok(IdleResponse::NewData(x)) => { info!(context, "{folder:?}: Idle has NewData {x:?}"); } - Ok(Event::IdleResponse(IdleResponse::Timeout)) => { + Ok(IdleResponse::Timeout) => { info!(context, "{folder:?}: Idle-wait timeout or interruption."); } - Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => { + Ok(IdleResponse::ManualInterrupt) => { info!(context, "{folder:?}: Idle wait was interrupted manually."); } - Ok(Event::Interrupt) => { - info!(context, "{folder:?}: Idle wait was interrupted."); - } Err(err) => { warn!(context, "{folder:?}: Idle wait errored: {err:?}."); } } + // Abort the task, then await to ensure the future is dropped. + interrupt_relay.abort(); + interrupt_relay.await.ok(); + let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done()) .await .with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))? diff --git a/src/imap/imap_tests.rs b/src/imap/imap_tests.rs new file mode 100644 index 0000000000..c2f5678b59 --- /dev/null +++ b/src/imap/imap_tests.rs @@ -0,0 +1,336 @@ +use super::*; +use crate::test_utils::TestContext; + +#[test] +fn test_get_folder_meaning_by_name() { + assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent); + assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent); + assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent); + assert_eq!( + get_folder_meaning_by_name("Messages envoyés"), + FolderMeaning::Sent + ); + assert_eq!( + get_folder_meaning_by_name("mEsSaGes envoyÉs"), + FolderMeaning::Sent + ); + assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown); + assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam); + assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_uid_next_validity() { + let t = TestContext::new_alice().await; + assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0); + assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0); + + set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap(); + assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7); + assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0); + + set_uid_next(&t.ctx, "Inbox", 5).await.unwrap(); + set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap(); + assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5); + assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6); +} + +#[test] +fn test_build_sequence_sets() { + assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]); + + let cases = vec![ + (vec![1], "1"), + (vec![3291], "3291"), + (vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"), + (vec![1, 2, 3], "1:3"), + (vec![1, 4, 5, 6], "1,4:6"), + ((1..=500).collect(), "1:500"), + (vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"), + ]; + for (input, s) in cases { + assert_eq!( + build_sequence_sets(&input).unwrap(), + vec![(input, s.into())] + ); + } + + let has_number = |(uids, s): &(Vec, String), number| { + uids.contains(&number) && s.split(',').any(|n| n.parse::().unwrap() == number) + }; + + let numbers: Vec<_> = (2..=500).step_by(2).collect(); + let result = build_sequence_sets(&numbers).unwrap(); + for (_, set) in &result { + assert!(set.len() < 1010); + assert!(!set.ends_with(',')); + assert!(!set.starts_with(',')); + } + assert!(result.len() == 1); // these UIDs fit in one set + for &number in &numbers { + assert!(result.iter().any(|r| has_number(r, number))); + } + + let numbers: Vec<_> = (1..=1000).step_by(3).collect(); + let result = build_sequence_sets(&numbers).unwrap(); + for (_, set) in &result { + assert!(set.len() < 1010); + assert!(!set.ends_with(',')); + assert!(!set.starts_with(',')); + } + let (last_uids, last_str) = result.last().unwrap(); + assert_eq!( + last_uids.get((last_uids.len() - 2)..).unwrap(), + &[997, 1000] + ); + assert!(last_str.ends_with("997,1000")); + assert!(result.len() == 2); // This time we need 2 sets + for &number in &numbers { + assert!(result.iter().any(|r| has_number(r, number))); + } + + let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect(); + let result = build_sequence_sets(&numbers).unwrap(); + for (_, set) in &result { + assert!(set.len() < 1010); + assert!(!set.ends_with(',')); + assert!(!set.starts_with(',')); + } + assert_eq!(result.len(), 6); + for &number in &numbers { + assert!(result.iter().any(|r| has_number(r, number))); + } +} + +async fn check_target_folder_combination( + folder: &str, + mvbox_move: bool, + chat_msg: bool, + expected_destination: &str, + accepted_chat: bool, + outgoing: bool, + setupmessage: bool, +) -> Result<()> { + println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"); + + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat")) + .await?; + t.ctx + .set_config(Config::ConfiguredSentboxFolder, Some("Sent")) + .await?; + t.ctx + .set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" })) + .await?; + + if accepted_chat { + let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?; + ChatId::create_for_contact(&t.ctx, contact_id).await?; + } + let temp; + + let bytes = if setupmessage { + include_bytes!("../../test-data/message/AutocryptSetupMessage.eml") + } else { + temp = format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + {}\ + Subject: foo\n\ + Message-ID: \n\ + {}\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + if outgoing { + "From: alice@example.org\nTo: bob@example.net\n" + } else { + "From: bob@example.net\nTo: alice@example.org\n" + }, + if chat_msg { "Chat-Version: 1.0\n" } else { "" }, + ); + temp.as_bytes() + }; + + let (headers, _) = mailparse::parse_headers(bytes)?; + let actual = if let Some(config) = + target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await? + { + t.get_config(config).await? + } else { + None + }; + + let expected = if expected_destination == folder { + None + } else { + Some(expected_destination) + }; + assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"); + Ok(()) +} + +// chat_msg means that the message was sent by Delta Chat +// The tuples are (folder, mvbox_move, chat_msg, expected_destination) +const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[ + ("INBOX", false, false, "INBOX"), + ("INBOX", false, true, "INBOX"), + ("INBOX", true, false, "INBOX"), + ("INBOX", true, true, "DeltaChat"), + ("Sent", false, false, "Sent"), + ("Sent", false, true, "Sent"), + ("Sent", true, false, "Sent"), + ("Sent", true, true, "DeltaChat"), + ("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs + ("Spam", false, true, "INBOX"), + ("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs + ("Spam", true, true, "DeltaChat"), +]; + +// These are the same as above, but non-chat messages in Spam stay in Spam +const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[ + ("INBOX", false, false, "INBOX"), + ("INBOX", false, true, "INBOX"), + ("INBOX", true, false, "INBOX"), + ("INBOX", true, true, "DeltaChat"), + ("Sent", false, false, "Sent"), + ("Sent", false, true, "Sent"), + ("Sent", true, false, "Sent"), + ("Sent", true, true, "DeltaChat"), + ("Spam", false, false, "Spam"), + ("Spam", false, true, "INBOX"), + ("Spam", true, false, "Spam"), + ("Spam", true, true, "DeltaChat"), +]; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_target_folder_incoming_accepted() -> Result<()> { + for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + check_target_folder_combination( + folder, + *mvbox_move, + *chat_msg, + expected_destination, + true, + false, + false, + ) + .await?; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_target_folder_incoming_request() -> Result<()> { + for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST { + check_target_folder_combination( + folder, + *mvbox_move, + *chat_msg, + expected_destination, + false, + false, + false, + ) + .await?; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_target_folder_outgoing() -> Result<()> { + // Test outgoing emails + for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + check_target_folder_combination( + folder, + *mvbox_move, + *chat_msg, + expected_destination, + true, + true, + false, + ) + .await?; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_target_folder_setupmsg() -> Result<()> { + // Test setupmessages + for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + check_target_folder_combination( + folder, + *mvbox_move, + *chat_msg, + if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam" + false, + true, + true, + ) + .await?; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_imap_search_command() -> Result<()> { + let t = TestContext::new_alice().await; + assert_eq!( + get_imap_self_sent_search_command(&t.ctx).await?, + r#"FROM "alice@example.org""# + ); + + t.ctx.set_primary_self_addr("alice@another.com").await?; + assert_eq!( + get_imap_self_sent_search_command(&t.ctx).await?, + r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"# + ); + + t.ctx.set_primary_self_addr("alice@third.com").await?; + assert_eq!( + get_imap_self_sent_search_command(&t.ctx).await?, + r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"# + ); + + Ok(()) +} + +#[test] +fn test_uid_grouper() { + // Input: sequence of (rowid: i64, uid: u32, target: String) + // Output: sequence of (target: String, rowid_set: Vec, uid_set: String) + let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]); + let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); + assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]); + + let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]); + let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); + assert_eq!( + res, + vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())] + ); + + let grouper = UidGrouper::from([ + (1, 2, "INBOX".to_string()), + (2, 2, "INBOX".to_string()), + (3, 3, "INBOX".to_string()), + ]); + let res: Vec<(String, Vec, String)> = grouper.into_iter().collect(); + assert_eq!( + res, + vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())] + ); +} + +#[test] +fn test_setmetadata_device_token() { + assert_eq!( + format_setmetadata("INBOX", "foobarbaz"), + "SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)" + ); + assert_eq!( + format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"), + "SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)" + ); +} diff --git a/src/imap/scan_folders.rs b/src/imap/scan_folders.rs index 58e9bbccf1..0f1a253ab1 100644 --- a/src/imap/scan_folders.rs +++ b/src/imap/scan_folders.rs @@ -17,16 +17,24 @@ impl Imap { session: &mut Session, ) -> Result { // 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 { - let elapsed_secs = time_elapsed(&last_scan).as_secs(); - let debounce_secs = context - .get_config_u64(Config::ScanAllFoldersDebounceSecs) - .await?; + { + let mut last_scan = context.last_full_folder_scan.lock().await; + if let Some(last_scan) = *last_scan { + let elapsed_secs = time_elapsed(&last_scan).as_secs(); + let debounce_secs = context + .get_config_u64(Config::ScanAllFoldersDebounceSecs) + .await?; - if elapsed_secs < debounce_secs { - return Ok(false); + if elapsed_secs < debounce_secs { + return Ok(false); + } } + + // Update the timestamp before scanning the folders + // to avoid holding the lock for too long. + // This means next scan is delayed even if + // the current one fails. + last_scan.replace(tools::Time::now()); } info!(context, "Starting full folder scan"); @@ -94,7 +102,6 @@ impl Imap { } info!(context, "Found folders: {folder_names:?}."); - last_scan.replace(tools::Time::now()); Ok(true) } } diff --git a/src/imap/select_folder.rs b/src/imap/select_folder.rs index c4849ada12..c40991834f 100644 --- a/src/imap/select_folder.rs +++ b/src/imap/select_folder.rs @@ -33,10 +33,10 @@ impl ImapSession { pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> { if let Some(folder) = &self.selected_folder { if self.selected_folder_needs_expunge { - info!(context, "Expunge messages in \"{}\".", folder); + info!(context, "Expunge messages in {folder:?}."); self.close().await.context("IMAP close/expunge failed")?; - info!(context, "close/expunge succeeded"); + info!(context, "Close/expunge succeeded."); self.selected_folder = None; self.selected_folder_needs_expunge = false; self.new_mail = false; @@ -73,6 +73,7 @@ impl ImapSession { match res { Ok(mailbox) => { + info!(context, "Selected folder {folder:?}."); self.selected_folder = Some(folder.to_string()); self.selected_mailbox = Some(mailbox); Ok(NewlySelected::Yes) @@ -94,7 +95,7 @@ impl ImapSession { Ok(newly_selected) => Ok(newly_selected), Err(err) => match err { Error::NoFolder(..) => { - info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder); + info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it."); let create_res = self.create(folder).await; if let Err(ref err) = create_res { info!(context, "Couldn't select folder, then create() failed: {err:#}."); @@ -129,7 +130,7 @@ impl ImapSession { let newly_selected = if create { self.select_or_create_folder(context, folder) .await - .with_context(|| format!("failed to select or create folder {folder}"))? + .with_context(|| format!("Failed to select or create folder {folder:?}"))? } else { match self.select_folder(context, folder).await { Ok(newly_selected) => newly_selected, @@ -137,7 +138,7 @@ impl ImapSession { Error::NoFolder(..) => return Ok(false), _ => { return Err(err) - .with_context(|| format!("failed to select folder {folder}"))? + .with_context(|| format!("Failed to select folder {folder:?}"))? } }, } @@ -145,14 +146,14 @@ impl ImapSession { let mailbox = self .selected_mailbox .as_mut() - .with_context(|| format!("No mailbox selected, folder: {folder}"))?; + .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}"))?; + .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}"))?; + .with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?; let new_uid_validity = mailbox .uid_validity diff --git a/src/imap/session.rs b/src/imap/session.rs index 7896476f25..74ad7aa757 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -1,4 +1,3 @@ -use std::cmp; use std::collections::BTreeMap; use std::ops::{Deref, DerefMut}; @@ -7,7 +6,6 @@ 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; @@ -143,33 +141,4 @@ impl Session { 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> { - 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()) - } } diff --git a/src/imex.rs b/src/imex.rs index aacc920795..dd8a589029 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -15,15 +15,15 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio_tar::Archive; use crate::blob::BlobDirContents; -use crate::chat::{self, delete_and_reset_all_device_msgs}; +use crate::chat::delete_and_reset_all_device_msgs; use crate::config::Config; use crate::context::Context; use crate::e2ee; use crate::events::EventType; use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey}; use crate::log::LogExt; -use crate::message::{Message, Viewtype}; use crate::pgp; +use crate::qr::DCBACKUP_VERSION; use crate::sql; use crate::tools::{ create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard, @@ -139,20 +139,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result { } } -async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { - if !context.sql.get_raw_config_bool("bcc_self").await? { - let mut msg = Message::new(Viewtype::Text); - // TODO: define this as a stockstring once the wording is settled. - msg.text = "It seems you are using multiple devices with Delta Chat. Great!\n\n\ - If you also want to synchronize outgoing messages across all devices, \ - go to \"Settings → Advanced\" and enable \"Send Copy to Self\"." - .to_string(); - chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?; - } - Ok(()) -} - -async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Result<()> { +async fn set_self_key(context: &Context, armored: &str) -> Result<()> { // try hard to only modify key-state let (private_key, header) = SignedSecretKey::from_asc(armored)?; let public_key = private_key.split_public_key()?; @@ -184,16 +171,7 @@ async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Re public: public_key, secret: private_key, }; - key::store_self_keypair( - context, - &keypair, - if set_default { - key::KeyPairUse::Default - } else { - key::KeyPairUse::ReadOnly - }, - ) - .await?; + key::store_self_keypair(context, &keypair).await?; info!(context, "stored self key: {:?}", keypair.secret.key_id()); Ok(()) @@ -223,7 +201,7 @@ async fn imex_inner( .await .context("Cannot create private key or private key not available")?; - create_folder(context, &path).await?; + create_folder(context, path).await?; } match what { @@ -415,6 +393,9 @@ async fn import_backup_stream_inner( .await .context("cannot import unpacked database"); } + if res.is_ok() { + res = check_backup_version(context).await; + } if res.is_ok() { res = adjust_bcc_self(context).await; } @@ -613,10 +594,10 @@ where } /// Imports secret key from a file. -async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> { - let buf = read_file(context, &path).await?; +async fn import_secret_key(context: &Context, path: &Path) -> Result<()> { + let buf = read_file(context, path).await?; let armored = std::string::String::from_utf8_lossy(&buf); - set_self_key(context, &armored, set_default).await?; + set_self_key(context, &armored).await?; Ok(()) } @@ -638,8 +619,7 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> { "Importing secret key from {} as the default key.", path.display() ); - let set_default = true; - import_secret_key(context, path, set_default).await?; + import_secret_key(context, path).await?; return Ok(()); } @@ -657,14 +637,13 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> { } else { continue; }; - let set_default = !name_f.contains("legacy"); info!( context, "Considering key file: {}.", path_plus_name.display() ); - if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await { + if let Err(err) = import_secret_key(context, &path_plus_name).await { warn!( context, "Failed to import secret key from {}: {:#}.", @@ -801,6 +780,10 @@ async fn export_database( .sql .set_raw_config_int("backup_time", timestamp) .await?; + context + .sql + .set_raw_config_int("backup_version", DCBACKUP_VERSION) + .await?; sql::housekeeping(context).await.log_err(context).ok(); context .sql @@ -838,6 +821,15 @@ async fn adjust_bcc_self(context: &Context) -> Result<()> { Ok(()) } +async fn check_backup_version(context: &Context) -> Result<()> { + let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2); + ensure!( + version <= DCBACKUP_VERSION, + "Backup too new, please update Delta Chat" + ); + Ok(()) +} + #[cfg(test)] mod tests { use std::time::Duration; @@ -885,7 +877,7 @@ mod tests { assert_eq!(bytes, key.to_asc(None).into_bytes()); - let alice = &TestContext::new_alice().await; + let alice = &TestContext::new().await; if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await { panic!("got error on import: {err:#}"); } @@ -907,7 +899,7 @@ mod tests { panic!("got error on export: {err:#}"); } - let context2 = TestContext::new_alice().await; + let context2 = TestContext::new().await; if let Err(err) = imex( &context2.ctx, ImexMode::ImportSelfKeys, @@ -934,15 +926,18 @@ mod tests { let alice = &TestContext::new_alice().await; let old_key = key::load_self_secret_key(alice).await?; - imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; - - let new_key = key::load_self_secret_key(alice).await?; - assert_ne!(new_key, old_key); - assert_eq!( - key::load_self_secret_keyring(alice).await?, - vec![new_key, old_key] + assert!( + imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None) + .await + .is_err() ); + // Importing a second key is not allowed anymore, + // even as a non-default key. + assert_eq!(key::load_self_secret_key(alice).await?, old_key); + + assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]); + let msg = alice.recv_msg(&sent).await; assert!(msg.get_showpadlock()); assert_eq!(msg.chat_id, alice.get_self_chat().await.id); diff --git a/src/imex/key_transfer.rs b/src/imex/key_transfer.rs index dbe63f9673..88f61742ed 100644 --- a/src/imex/key_transfer.rs +++ b/src/imex/key_transfer.rs @@ -6,16 +6,15 @@ use anyhow::{bail, ensure, Result}; use crate::blob::BlobObject; use crate::chat::{self, ChatId}; use crate::config::Config; +use crate::constants::{ASM_BODY, ASM_SUBJECT}; use crate::contact::ContactId; use crate::context::Context; -use crate::imex::maybe_add_bcc_self_device_msg; use crate::imex::set_self_key; use crate::key::{load_self_secret_key, DcKey}; use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::pgp; -use crate::stock_str; use crate::tools::open_file_std; /// Initiates key transfer via Autocrypt Setup Message. @@ -33,26 +32,22 @@ pub async fn initiate_key_transfer(context: &Context) -> Result { )?; let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?; - let mut msg = Message { - viewtype: Viewtype::File, - ..Default::default() - }; + let mut msg = Message::new(Viewtype::File); msg.param.set(Param::File, setup_file_blob.as_name()); msg.param .set(Param::Filename, "autocrypt-setup-message.html"); - msg.subject = stock_str::ac_setup_msg_subject(context).await; + msg.subject = ASM_SUBJECT.to_owned(); msg.param .set(Param::MimeType, "application/autocrypt-setup"); msg.param.set_cmd(SystemMessage::AutocryptSetupMessage); msg.force_plaintext(); msg.param.set_int(Param::SkipAutocrypt, 1); + // Enable BCC-self, because transferring a key + // means we have a multi-device setup. + context.set_config_bool(Config::BccSelf, true).await?; + chat::send_msg(context, chat_id, &mut msg).await?; - // no maybe_add_bcc_self_device_msg() here. - // the ui shows the dialog with the setup code on this device, - // it would be too much noise to have two things popping up at the same time. - // maybe_add_bcc_self_device_msg() is called on the other device - // once the transfer is completed. Ok(setup_code) } @@ -77,8 +72,8 @@ pub async fn continue_key_transfer( let file = open_file_std(context, filename)?; let sc = normalize_setup_code(setup_code); let armored_key = decrypt_setup_file(&sc, file).await?; - set_self_key(context, &armored_key, true).await?; - maybe_add_bcc_self_device_msg(context).await?; + set_self_key(context, &armored_key).await?; + context.set_config_bool(Config::BccSelf, true).await?; Ok(()) } else { @@ -115,8 +110,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result"); Ok(format!( concat!( @@ -189,7 +184,6 @@ mod tests { use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE}; use crate::receive_imf::receive_imf; - use crate::stock_str::StockMessage; use crate::test_utils::{TestContext, TestContextManager}; use ::pgp::armor::BlockType; @@ -215,12 +209,9 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_render_setup_file_newline_replace() { let t = TestContext::new_alice().await; - t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string()) - .await - .unwrap(); let msg = render_setup_file(&t, "pw").await.unwrap(); println!("{}", &msg); - assert!(msg.contains("

hello
there

")); + assert!(msg.contains("

This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.
")); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -301,8 +292,12 @@ mod tests { async fn test_key_transfer() -> Result<()> { let alice = TestContext::new_alice().await; + alice.set_config(Config::BccSelf, Some("0")).await?; let setup_code = initiate_key_transfer(&alice).await?; + // Test that sending Autocrypt Setup Message enables `bcc_self`. + assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true); + // Get Autocrypt Setup Message. let sent = alice.pop_sent_msg().await; @@ -313,21 +308,24 @@ mod tests { 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. - let sent = alice2.send_text(msg.chat_id, "Test").await; - let trashed_message = alice.recv_msg_opt(&sent).await; - assert!(trashed_message.is_none()); - assert_ne!(alice.get_last_msg().await.get_text(), "Test"); + assert_eq!( + crate::key::load_self_secret_keyring(&alice2).await?.len(), + 0 + ); // Transfer the key. + alice2.set_config(Config::BccSelf, Some("0")).await?; continue_key_transfer(&alice2, msg.id, &setup_code).await?; + assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true); + assert_eq!( + crate::key::load_self_secret_keyring(&alice2).await?.len(), + 1 + ); // Alice sends a message to self from the new device. let sent = alice2.send_text(msg.chat_id, "Test").await; - alice.recv_msg(&sent).await; - assert_eq!(alice.get_last_msg().await.get_text(), "Test"); + let rcvd_msg = alice.recv_msg(&sent).await; + assert_eq!(rcvd_msg.get_text(), "Test"); Ok(()) } diff --git a/src/imex/transfer.rs b/src/imex/transfer.rs index 5295ced215..69673cd299 100644 --- a/src/imex/transfer.rs +++ b/src/imex/transfer.rs @@ -45,7 +45,7 @@ use crate::message::Message; use crate::qr::Qr; use crate::stock_str::backup_transfer_msg_body; use crate::tools::{create_id, time, TempPathGuard}; -use crate::EventType; +use crate::{e2ee, EventType}; use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME}; @@ -95,6 +95,7 @@ impl BackupProvider { pub async fn prepare(context: &Context) -> Result { let relay_mode = RelayMode::Disabled; let endpoint = Endpoint::builder() + .tls_x509() // For compatibility with iroh <0.34.0 .alpns(vec![BACKUP_ALPN.to_vec()]) .relay_mode(relay_mode) .bind() @@ -109,6 +110,11 @@ impl BackupProvider { .parent() .context("Context dir not found")?; + // before we export, make sure the private key exists + e2ee::ensure_secret_key_exists(context) + .await + .context("Cannot create private key or private key not available")?; + let dbfile = context_dir.join(DBFILE_BACKUP_NAME); if fs::metadata(&dbfile).await.is_ok() { fs::remove_file(&dbfile).await?; @@ -296,7 +302,11 @@ pub async fn get_backup2( ) -> Result<()> { let relay_mode = RelayMode::Disabled; - let endpoint = Endpoint::builder().relay_mode(relay_mode).bind().await?; + let endpoint = Endpoint::builder() + .tls_x509() // For compatibility with iroh <0.34.0 + .relay_mode(relay_mode) + .bind() + .await?; let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?; let (mut send_stream, mut recv_stream) = conn.open_bi().await?; diff --git a/src/key.rs b/src/key.rs index dc3aca7995..29a2a5d9d5 100644 --- a/src/key.rs +++ b/src/key.rs @@ -7,7 +7,6 @@ use std::io::Cursor; use anyhow::{bail, ensure, Context as _, Result}; use base64::Engine as _; use deltachat_contact_tools::EmailAddress; -use num_traits::FromPrimitive; use pgp::composed::Deserializable; pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; use pgp::ser::Serialize; @@ -15,8 +14,6 @@ use pgp::types::{PublicKeyTrait, SecretKeyTrait}; use rand::thread_rng; use tokio::runtime::Handle; -use crate::config::Config; -use crate::constants::KeyGenType; use crate::context::Context; use crate::log::LogExt; use crate::pgp::KeyPair; @@ -282,14 +279,12 @@ async fn generate_keypair(context: &Context) -> Result { Some(key_pair) => Ok(key_pair), None => { let start = tools::Time::now(); - let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?) - .unwrap_or_default(); - info!(context, "Generating keypair with type {}", keytype); + info!(context, "Generating keypair."); let keypair = Handle::current() - .spawn_blocking(move || crate::pgp::create_keypair(addr, keytype)) + .spawn_blocking(move || crate::pgp::create_keypair(addr)) .await??; - store_self_keypair(context, &keypair, KeyPairUse::Default).await?; + store_self_keypair(context, &keypair).await?; info!( context, "Keypair generated in {:.3}s.", @@ -326,18 +321,6 @@ pub(crate) async fn load_keypair(context: &Context) -> Result> { }) } -/// Use of a key pair for encryption or decryption. -/// -/// This is used by `store_self_keypair` to know what kind of key is -/// being saved. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum KeyPairUse { - /// The default key used to encrypt new messages. - Default, - /// Only used to decrypt existing message. - ReadOnly, -} - /// Store the keypair as an owned keypair for addr in the database. /// /// This will save the keypair as keys for the given address. The @@ -350,11 +333,7 @@ pub enum KeyPairUse { /// same key again overwrites it. /// /// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr -pub(crate) async fn store_self_keypair( - context: &Context, - keypair: &KeyPair, - default: KeyPairUse, -) -> Result<()> { +pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<()> { let mut config_cache_lock = context.sql.config_cache.write().await; let new_key_id = context .sql @@ -362,29 +341,28 @@ pub(crate) async fn store_self_keypair( let public_key = DcKey::to_bytes(&keypair.public); let secret_key = DcKey::to_bytes(&keypair.secret); - let is_default = match default { - KeyPairUse::Default => true, - KeyPairUse::ReadOnly => false, - }; - + // private_key and public_key columns + // are UNIQUE since migration 107, + // so this fails if we already have this key. transaction .execute( - "INSERT OR REPLACE INTO keypairs (public_key, private_key) + "INSERT INTO keypairs (public_key, private_key) VALUES (?,?)", (&public_key, &secret_key), ) .context("Failed to insert keypair")?; - if is_default { - let new_key_id = transaction.last_insert_rowid(); - transaction.execute( - "INSERT OR REPLACE INTO config (keyname, value) VALUES ('key_id', ?)", - (new_key_id,), - )?; - Ok(Some(new_key_id)) - } else { - Ok(None) - } + let new_key_id = transaction.last_insert_rowid(); + + // This will fail if we already have `key_id`. + // + // Setting default key is only possible if we don't + // have a key already. + transaction.execute( + "INSERT INTO config (keyname, value) VALUES ('key_id', ?)", + (new_key_id,), + )?; + Ok(Some(new_key_id)) }) .await?; @@ -405,7 +383,7 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul let secret = SignedSecretKey::from_asc(secret_data)?.0; let public = secret.split_public_key()?; let keypair = KeyPair { public, secret }; - store_self_keypair(context, &keypair, KeyPairUse::Default).await?; + store_self_keypair(context, &keypair).await?; Ok(()) } @@ -478,14 +456,13 @@ impl std::str::FromStr for Fingerprint { #[cfg(test)] mod tests { - use std::sync::Arc; - - use once_cell::sync::Lazy; + use std::sync::{Arc, LazyLock}; use super::*; + use crate::config::Config; use crate::test_utils::{alice_keypair, TestContext}; - static KEYPAIR: Lazy = Lazy::new(alice_keypair); + static KEYPAIR: LazyLock = LazyLock::new(alice_keypair); #[test] fn test_from_armored_string() { @@ -635,16 +612,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD assert_eq!(key, key2); } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_load_self_existing() { - let alice = alice_keypair(); - let t = TestContext::new_alice().await; - let pubkey = load_self_public_key(&t).await.unwrap(); - assert_eq!(alice.public, pubkey); - let seckey = load_self_secret_key(&t).await.unwrap(); - assert_eq!(alice.secret, seckey); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_load_self_generate_public() { let t = TestContext::new().await; @@ -700,6 +667,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key); } + /// Tests that setting a default key second time is not allowed. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_save_self_key_twice() { // Saving the same key twice should result in only one row in @@ -714,13 +682,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD .unwrap() }; assert_eq!(nrows().await, 0); - store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default) - .await - .unwrap(); + store_self_keypair(&ctx, &KEYPAIR).await.unwrap(); assert_eq!(nrows().await, 1); - store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default) - .await - .unwrap(); + + // Saving a second key fails. + let res = store_self_keypair(&ctx, &KEYPAIR).await; + assert!(res.is_err()); + assert_eq!(nrows().await, 1); } diff --git a/src/lib.rs b/src/lib.rs index 86c79ebdaa..9e9cc49cca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ pub(crate) mod events; pub use events::*; mod aheader; -mod blob; +pub mod blob; pub mod chat; pub mod chatlist; pub mod config; @@ -68,7 +68,7 @@ mod imap; pub mod imex; pub mod key; pub mod location; -mod login_param; +pub mod login_param; pub mod message; mod mimefactory; pub mod mimeparser; @@ -116,6 +116,3 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; mod test_utils; #[cfg(test)] mod tests; - -#[cfg(fuzzing)] -pub mod fuzzing; diff --git a/src/login_param.rs b/src/login_param.rs index 7ee7887a97..f91ffa59e4 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -2,23 +2,38 @@ use std::fmt; -use anyhow::{format_err, Context as _, Result}; -use deltachat_contact_tools::EmailAddress; +use anyhow::{bail, ensure, format_err, Context as _, Result}; +use deltachat_contact_tools::{addr_cmp, addr_normalize, EmailAddress}; +use num_traits::ToPrimitive as _; use serde::{Deserialize, Serialize}; use crate::config::Config; use crate::configure::server_params::{expand_param_vector, ServerParams}; -use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; +use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2}; use crate::context::Context; use crate::net::load_connection_timestamp; -use crate::net::proxy::ProxyConfig; -use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; +pub use crate::net::proxy::ProxyConfig; +pub use crate::provider::Socket; +use crate::provider::{get_provider_by_id, Protocol, Provider, UsernamePattern}; use crate::sql::Sql; +use crate::tools::ToOption; /// User-entered setting for certificate checks. /// /// Should be saved into `imap_certificate_checks` before running configuration. -#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] +#[derive( + Copy, + Clone, + Debug, + Default, + Display, + FromPrimitive, + ToPrimitive, + PartialEq, + Eq, + Serialize, + Deserialize, +)] #[repr(u32)] #[strum(serialize_all = "snake_case")] pub enum EnteredCertificateChecks { @@ -41,10 +56,12 @@ pub enum EnteredCertificateChecks { } /// Values saved into `imap_certificate_checks`. -#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] +#[derive( + Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize, +)] #[repr(u32)] #[strum(serialize_all = "snake_case")] -pub enum ConfiguredCertificateChecks { +pub(crate) enum ConfiguredCertificateChecks { /// Use configuration from the provider database. /// If there is no provider database setting for certificate checks, /// accept invalid certificates. @@ -78,7 +95,7 @@ pub enum ConfiguredCertificateChecks { } /// Login parameters for a single server, either IMAP or SMTP -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EnteredServerLoginParam { /// Server hostname or IP address. pub server: String, @@ -101,7 +118,7 @@ pub struct EnteredServerLoginParam { } /// Login parameters entered by the user. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EnteredLoginParam { /// Email address. pub addr: String, @@ -116,15 +133,13 @@ pub struct EnteredLoginParam { /// invalid hostnames pub certificate_checks: EnteredCertificateChecks, - /// Proxy configuration. - pub proxy_config: Option, - + /// If true, login via OAUTH2 (not recommended anymore) pub oauth2: bool, } impl EnteredLoginParam { /// Loads entered account settings. - pub async fn load(context: &Context) -> Result { + pub(crate) async fn load(context: &Context) -> Result { let addr = context .get_config(Config::Addr) .await? @@ -196,8 +211,6 @@ impl EnteredLoginParam { .unwrap_or_default(); let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); - let proxy_config = ProxyConfig::load(context).await?; - Ok(EnteredLoginParam { addr, imap: EnteredServerLoginParam { @@ -215,10 +228,71 @@ impl EnteredLoginParam { password: send_pw, }, certificate_checks, - proxy_config, oauth2, }) } + + /// Saves entered account settings, + /// so that they can be prefilled if the user wants to configure the server again. + pub(crate) async fn save(&self, context: &Context) -> Result<()> { + context.set_config(Config::Addr, Some(&self.addr)).await?; + + context + .set_config(Config::MailServer, self.imap.server.to_option()) + .await?; + context + .set_config(Config::MailPort, self.imap.port.to_option().as_deref()) + .await?; + context + .set_config( + Config::MailSecurity, + self.imap.security.to_i32().to_option().as_deref(), + ) + .await?; + context + .set_config(Config::MailUser, self.imap.user.to_option()) + .await?; + context + .set_config(Config::MailPw, self.imap.password.to_option()) + .await?; + + context + .set_config(Config::SendServer, self.smtp.server.to_option()) + .await?; + context + .set_config(Config::SendPort, self.smtp.port.to_option().as_deref()) + .await?; + context + .set_config( + Config::SendSecurity, + self.smtp.security.to_i32().to_option().as_deref(), + ) + .await?; + context + .set_config(Config::SendUser, self.smtp.user.to_option()) + .await?; + context + .set_config(Config::SendPw, self.smtp.password.to_option()) + .await?; + + context + .set_config( + Config::ImapCertificateChecks, + self.certificate_checks.to_i32().to_option().as_deref(), + ) + .await?; + + let server_flags = if self.oauth2 { + Some(DC_LP_AUTH_OAUTH2.to_string()) + } else { + None + }; + context + .set_config(Config::ServerFlags, server_flags.as_deref()) + .await?; + + Ok(()) + } } impl fmt::Display for EnteredLoginParam { @@ -319,7 +393,7 @@ impl TryFrom for ConnectionSecurity { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConfiguredServerLoginParam { +pub(crate) struct ConfiguredServerLoginParam { pub connection: ConnectionCandidate, /// Username. @@ -357,7 +431,7 @@ pub(crate) async fn prioritize_server_login_params( /// Login parameters saved to the database /// after successful configuration. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConfiguredLoginParam { +pub(crate) struct ConfiguredLoginParam { /// `From:` address that was used at the time of configuration. pub addr: String, @@ -381,15 +455,29 @@ pub struct ConfiguredLoginParam { pub smtp_password: String, - /// Proxy configuration. - pub proxy_config: Option, - pub provider: Option<&'static Provider>, /// TLS options: whether to allow invalid certificates and/or /// invalid hostnames pub certificate_checks: ConfiguredCertificateChecks, + /// If true, login via OAUTH2 (not recommended anymore) + pub oauth2: bool, +} + +/// The representation of ConfiguredLoginParam in the database, +/// saved as Json. +#[derive(Debug, Serialize, Deserialize)] +struct ConfiguredLoginParamJson { + pub addr: String, + pub imap: Vec, + pub imap_user: String, + pub imap_password: String, + pub smtp: Vec, + pub smtp_user: String, + pub smtp_password: String, + pub provider_id: Option, + pub certificate_checks: ConfiguredCertificateChecks, pub oauth2: bool, } @@ -428,7 +516,27 @@ impl ConfiguredLoginParam { /// Load configured account settings from the database. /// /// Returns `None` if account is not configured. - pub async fn load(context: &Context) -> Result> { + pub(crate) async fn load(context: &Context) -> Result> { + let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else { + return Ok(None); + }; + + let json: Option = context + .sql + .query_get_value( + "SELECT configured_param FROM transports WHERE addr=?", + (&self_addr,), + ) + .await?; + if let Some(json) = json { + Ok(Some(Self::from_json(&json)?)) + } else { + bail!("Self address {self_addr} doesn't have a corresponding transport"); + } + } + + /// Loads legacy configured param. Only used for tests and the migration. + pub(crate) async fn load_legacy(context: &Context) -> Result> { if !context.get_config_bool(Config::Configured).await? { return Ok(None); } @@ -681,8 +789,6 @@ impl ConfiguredLoginParam { }]; } - let proxy_config = ProxyConfig::load(context).await?; - Ok(Some(ConfiguredLoginParam { addr, imap, @@ -693,94 +799,91 @@ impl ConfiguredLoginParam { smtp_password: send_pw, certificate_checks, provider, - proxy_config, oauth2, })) } - /// Save this loginparam to the database. - pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> { - context.set_primary_self_addr(&self.addr).await?; - + pub(crate) async fn save_to_transports_table( + self, + context: &Context, + entered_param: &EnteredLoginParam, + ) -> Result<()> { + let addr = addr_normalize(&self.addr); + let configured_addr = context.get_config(Config::ConfiguredAddr).await?; + if let Some(configured_addr) = configured_addr { + ensure!( + addr_cmp(&configured_addr, &addr,), + "Adding a second transport is not supported right now." + ); + } context - .set_config( - Config::ConfiguredImapServers, - Some(&serde_json::to_string(&self.imap)?), + .sql + .set_raw_config( + Config::ConfiguredProvider.as_ref(), + self.provider.map(|provider| provider.id), ) .await?; context - .set_config( - Config::ConfiguredSmtpServers, - Some(&serde_json::to_string(&self.smtp)?), + .sql + .execute( + "INSERT INTO transports (addr, entered_param, configured_param) + VALUES (?, ?, ?) + ON CONFLICT (addr) + DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param", + ( + self.addr.clone(), + serde_json::to_string(entered_param)?, + self.into_json()?, + ), ) .await?; - - context - .set_config(Config::ConfiguredMailUser, Some(&self.imap_user)) - .await?; context - .set_config(Config::ConfiguredMailPw, Some(&self.imap_password)) + .sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) .await?; + Ok(()) + } - context - .set_config(Config::ConfiguredSendUser, Some(&self.smtp_user)) - .await?; - context - .set_config(Config::ConfiguredSendPw, Some(&self.smtp_password)) - .await?; + pub(crate) fn from_json(json: &str) -> Result { + let json: ConfiguredLoginParamJson = serde_json::from_str(json)?; - context - .set_config_u32( - Config::ConfiguredImapCertificateChecks, - self.certificate_checks as u32, - ) - .await?; - context - .set_config_u32( - Config::ConfiguredSmtpCertificateChecks, - self.certificate_checks as u32, - ) - .await?; + let provider = json.provider_id.and_then(|id| get_provider_by_id(&id)); - // Remove legacy settings. - context - .set_config(Config::ConfiguredMailServer, None) - .await?; - context.set_config(Config::ConfiguredMailPort, None).await?; - context - .set_config(Config::ConfiguredMailSecurity, None) - .await?; - context - .set_config(Config::ConfiguredSendServer, None) - .await?; - context.set_config(Config::ConfiguredSendPort, None).await?; - context - .set_config(Config::ConfiguredSendSecurity, None) - .await?; + Ok(ConfiguredLoginParam { + addr: json.addr, + imap: json.imap, + imap_user: json.imap_user, + imap_password: json.imap_password, + smtp: json.smtp, + smtp_user: json.smtp_user, + smtp_password: json.smtp_password, + provider, + certificate_checks: json.certificate_checks, + oauth2: json.oauth2, + }) + } - let server_flags = match self.oauth2 { - true => DC_LP_AUTH_OAUTH2, - false => DC_LP_AUTH_NORMAL, + pub(crate) fn into_json(self) -> Result { + let json = ConfiguredLoginParamJson { + addr: self.addr, + imap: self.imap, + imap_user: self.imap_user, + imap_password: self.imap_password, + smtp: self.smtp, + smtp_user: self.smtp_user, + smtp_password: self.smtp_password, + provider_id: self.provider.map(|p| p.id.to_string()), + certificate_checks: self.certificate_checks, + oauth2: self.oauth2, }; - context - .set_config_u32(Config::ConfiguredServerFlags, server_flags as u32) - .await?; - - context - .set_config( - Config::ConfiguredProvider, - self.provider.map(|provider| provider.id), - ) - .await?; - - Ok(()) + Ok(serde_json::to_string(&json)?) } - pub fn strict_tls(&self) -> bool { + pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool { let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); match self.certificate_checks { ConfiguredCertificateChecks::OldAutomatic => { - provider_strict_tls.unwrap_or(self.proxy_config.is_some()) + provider_strict_tls.unwrap_or(connected_through_proxy) } ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true), ConfiguredCertificateChecks::Strict => true, @@ -793,8 +896,10 @@ impl ConfiguredLoginParam { #[cfg(test)] mod tests { use super::*; + use crate::log::LogExt as _; use crate::provider::get_provider_by_id; use crate::test_utils::TestContext; + use pretty_assertions::assert_eq; #[test] fn test_certificate_checks_display() { @@ -839,6 +944,42 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_save_entered_login_param() -> Result<()> { + let t = TestContext::new().await; + let param = EnteredLoginParam { + addr: "alice@example.org".to_string(), + imap: EnteredServerLoginParam { + server: "".to_string(), + port: 0, + security: Socket::Starttls, + user: "".to_string(), + password: "foobar".to_string(), + }, + smtp: EnteredServerLoginParam { + server: "".to_string(), + port: 2947, + security: Socket::default(), + user: "".to_string(), + password: "".to_string(), + }, + certificate_checks: Default::default(), + oauth2: false, + }; + param.save(&t).await?; + assert_eq!( + t.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar"); + assert_eq!(t.get_config(Config::SendPw).await?, None); + assert_eq!(t.get_config_int(Config::SendPort).await?, 2947); + + assert_eq!(EnteredLoginParam::load(&t).await?, param); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_save_load_login_param() -> Result<()> { let t = TestContext::new().await; @@ -865,25 +1006,38 @@ mod tests { }], smtp_user: "".to_string(), smtp_password: "bar".to_string(), - // proxy_config is not saved by `save_to_database`, using default value - proxy_config: None, provider: None, certificate_checks: ConfiguredCertificateChecks::Strict, oauth2: false, }; - param.save_as_configured_params(&t).await?; + param + .clone() + .save_to_transports_table(&t, &EnteredLoginParam::default()) + .await?; + let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#; assert_eq!( - t.get_config(Config::ConfiguredImapServers).await?.unwrap(), - r#"[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}]"# + t.sql + .query_get_value::("SELECT configured_param FROM transports", ()) + .await? + .unwrap(), + expected_param ); - t.set_config(Config::Configured, Some("1")).await?; + assert_eq!(t.is_configured().await?, true); let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); assert_eq!(param, loaded); - // Test that we don't panic on unknown ConfiguredImapCertificateChecks values. + // Legacy ConfiguredImapCertificateChecks config is ignored t.set_config(Config::ConfiguredImapCertificateChecks, Some("999")) .await?; + assert!(ConfiguredLoginParam::load(&t).await.is_ok()); + + // Test that we don't panic on unknown ConfiguredImapCertificateChecks values. + let wrong_param = expected_param.replace("Strict", "Stricct"); + assert_ne!(expected_param, wrong_param); + t.sql + .execute("UPDATE transports SET configured_param=?", (wrong_param,)) + .await?; assert!(ConfiguredLoginParam::load(&t).await.is_err()); Ok(()) @@ -900,7 +1054,8 @@ mod tests { t.set_config(Config::Configured, Some("1")).await?; t.set_config(Config::ConfiguredProvider, Some("posteo")) .await?; - t.set_config(Config::ConfiguredAddr, Some("alice@posteo.at")) + t.sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at")) .await?; t.set_config(Config::ConfiguredMailServer, Some("posteo.de")) .await?; @@ -969,12 +1124,15 @@ mod tests { ], smtp_user: "alice@posteo.de".to_string(), smtp_password: "foobarbaz".to_string(), - proxy_config: None, provider: get_provider_by_id("posteo"), certificate_checks: ConfiguredCertificateChecks::Strict, oauth2: false, }; + let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); + assert_eq!(loaded, param); + + migrate_configured_login_param(&t).await; let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); assert_eq!(loaded, param); @@ -982,7 +1140,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_empty_server_list() -> Result<()> { + async fn test_empty_server_list_legacy() -> Result<()> { // Find a provider that does not have server list set. // // There is at least one such provider in the provider database. @@ -998,7 +1156,9 @@ mod tests { t.set_config(Config::Configured, Some("1")).await?; t.set_config(Config::ConfiguredProvider, Some(provider.id)) .await?; - t.set_config(Config::ConfiguredAddr, Some(&addr)).await?; + t.sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) + .await?; t.set_config(Config::ConfiguredMailPw, Some("foobarbaz")) .await?; t.set_config(Config::ConfiguredImapCertificateChecks, Some("1")) @@ -1010,10 +1170,76 @@ mod tests { t.set_config(Config::ConfiguredServerFlags, Some("0")) .await?; + let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); + assert_eq!(loaded.provider, Some(*provider)); + assert_eq!(loaded.imap.is_empty(), false); + assert_eq!(loaded.smtp.is_empty(), false); + + migrate_configured_login_param(&t).await; + + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); + assert_eq!(loaded.provider, Some(*provider)); + assert_eq!(loaded.imap.is_empty(), false); + assert_eq!(loaded.smtp.is_empty(), false); + + Ok(()) + } + + async fn migrate_configured_login_param(t: &TestContext) { + t.sql.execute("DROP TABLE transports;", ()).await.unwrap(); + t.sql.set_raw_config_int("dbversion", 130).await.unwrap(); + t.sql.run_migrations(t).await.log_err(t).ok(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_empty_server_list() -> Result<()> { + // Find a provider that does not have server list set. + // + // There is at least one such provider in the provider database. + let (domain, provider) = crate::provider::data::PROVIDER_DATA + .iter() + .find(|(_domain, provider)| provider.server.is_empty()) + .unwrap(); + + let t = TestContext::new().await; + + let addr = format!("alice@{domain}"); + + ConfiguredLoginParam { + addr: addr.clone(), + imap: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "example.org".to_string(), + port: 100, + security: ConnectionSecurity::Tls, + }, + user: addr.clone(), + }], + imap_user: addr.clone(), + imap_password: "foobarbaz".to_string(), + smtp: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "example.org".to_string(), + port: 100, + security: ConnectionSecurity::Tls, + }, + user: addr.clone(), + }], + smtp_user: addr.clone(), + smtp_password: "foobarbaz".to_string(), + provider: Some(provider), + certificate_checks: ConfiguredCertificateChecks::Automatic, + oauth2: false, + } + .save_to_transports_table(&t, &EnteredLoginParam::default()) + .await?; + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); assert_eq!(loaded.provider, Some(*provider)); assert_eq!(loaded.imap.is_empty(), false); assert_eq!(loaded.smtp.is_empty(), false); + assert_eq!(t.get_configured_provider().await?, Some(*provider)); + Ok(()) } } diff --git a/src/message.rs b/src/message.rs index cfe01b717a..5e5192446d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,6 +1,7 @@ //! # Messages and their identifiers. use std::collections::BTreeSet; +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::str; @@ -11,11 +12,11 @@ use serde::{Deserialize, Serialize}; use tokio::{fs, io}; use crate::blob::BlobObject; -use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility}; +use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility}; use crate::chatlist_events; use crate::config::Config; use crate::constants::{ - Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL, + Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL, }; use crate::contact::{self, Contact, ContactId}; use crate::context::Context; @@ -31,9 +32,11 @@ use crate::pgp::split_armored_data; use crate::reaction::get_msg_reactions; use crate::sql; use crate::summary::Summary; +use crate::sync::SyncData; +use crate::tools::create_outgoing_rfc724_mid; use crate::tools::{ - buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file, time, - timestamp_to_str, truncate, + buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file, + sanitize_filename, time, timestamp_to_str, }; /// Message ID, including reserved IDs. @@ -172,15 +175,6 @@ impl MsgId { self.0 } - /// Returns raw text of a message, used for message info - pub async fn rawtext(self, context: &Context) -> Result { - Ok(context - .sql - .query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,)) - .await? - .unwrap_or_default()) - } - /// Returns server foldernames and UIDs of a message, used for message info pub async fn get_info_server_urls( context: &Context, @@ -217,12 +211,9 @@ impl MsgId { /// Returns detailed message information in a multi-line text form. pub async fn get_info(self, context: &Context) -> Result { let msg = Message::load_from_db(context, self).await?; - let rawtxt: String = self.rawtext(context).await?; let mut ret = String::new(); - let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN); - let fts = timestamp_to_str(msg.get_timestamp()); ret += &format!("Sent: {fts}"); @@ -333,9 +324,6 @@ impl MsgId { if duration != 0 { ret += &format!("Duration: {duration} ms\n",); } - if !rawtxt.is_empty() { - ret += &format!("\n{rawtxt}\n"); - } if !msg.rfc724_mid.is_empty() { ret += &format!("\nMessage-ID: {}", msg.rfc724_mid); @@ -483,6 +471,7 @@ impl Message { pub fn new(viewtype: Viewtype) -> Self { Message { viewtype, + rfc724_mid: create_outgoing_rfc724_mid(), ..Default::default() } } @@ -492,6 +481,7 @@ impl Message { Message { viewtype: Viewtype::Text, text, + rfc724_mid: create_outgoing_rfc724_mid(), ..Default::default() } } @@ -636,7 +626,7 @@ impl Message { /// Returns the full path to the file associated with a message. pub fn get_file(&self, context: &Context) -> Option { - self.param.get_path(Param::File, context).unwrap_or(None) + self.param.get_file_path(context).unwrap_or(None) } /// Returns vector of vcards if the file has a vCard attachment. @@ -669,7 +659,7 @@ impl Message { /// If message is an image or gif, set Param::Width and Param::Height pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { if self.viewtype.has_file() { - let file_param = self.param.get_path(Param::File, context)?; + let file_param = self.param.get_file_path(context)?; if let Some(path_and_filename) = file_param { if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif) && !self.param.exists(Param::Width) @@ -807,17 +797,17 @@ impl Message { /// To get the full path, use [`Self::get_file()`]. pub fn get_filename(&self) -> Option { if let Some(name) = self.param.get(Param::Filename) { - return Some(name.to_string()); + return Some(sanitize_filename(name)); } self.param .get(Param::File) .and_then(|file| Path::new(file).file_name()) - .map(|name| name.to_string_lossy().to_string()) + .map(|name| sanitize_filename(&name.to_string_lossy())) } /// Returns the size of the file in bytes, if applicable. pub async fn get_filebytes(&self, context: &Context) -> Result> { - if let Some(path) = self.param.get_path(Param::File, context)? { + if let Some(path) = self.param.get_file_path(context)? { Ok(Some(get_filebytes(context, &path).await.with_context( || format!("failed to get {} size in bytes", path.display()), )?)) @@ -931,6 +921,11 @@ impl Message { 0 != self.param.get_int(Param::Forwarded).unwrap_or_default() } + /// Returns true if the message is edited. + pub fn is_edited(&self) -> bool { + self.param.get_bool(Param::IsEdited).unwrap_or_default() + } + /// Returns true if the message is an informational message. pub fn is_info(&self) -> bool { let cmd = self.param.get_cmd(); @@ -944,6 +939,51 @@ impl Message { self.param.get_cmd() } + /// Return the contact ID of the profile to open when tapping the info message. + pub async fn get_info_contact_id(&self, context: &Context) -> Result> { + match self.param.get_cmd() { + SystemMessage::GroupNameChanged + | SystemMessage::GroupImageChanged + | SystemMessage::EphemeralTimerChanged => { + if self.from_id != ContactId::INFO { + Ok(Some(self.from_id)) + } else { + Ok(None) + } + } + + SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => { + if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) { + let contact_id = ContactId::new(contact_i32.try_into()?); + if contact_id == ContactId::SELF + || Contact::real_exists_by_id(context, contact_id).await? + { + Ok(Some(contact_id)) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + SystemMessage::AutocryptSetupMessage + | SystemMessage::SecurejoinMessage + | SystemMessage::LocationStreamingEnabled + | SystemMessage::LocationOnly + | SystemMessage::ChatProtectionEnabled + | SystemMessage::ChatProtectionDisabled + | SystemMessage::InvalidUnencryptedMail + | SystemMessage::SecurejoinWait + | SystemMessage::SecurejoinWaitTimeout + | SystemMessage::MultiDeviceSync + | SystemMessage::WebxdcStatusUpdate + | SystemMessage::WebxdcInfoMessage + | SystemMessage::IrohNodeAddr + | SystemMessage::Unknown => Ok(None), + } + } + /// Returns true if the message is a system message. pub fn is_system_message(&self) -> bool { let cmd = self.param.get_cmd(); @@ -968,7 +1008,7 @@ impl Message { } if let Some(filename) = self.get_file(context) { - if let Ok(ref buf) = read_file(context, filename).await { + if let Ok(ref buf) = read_file(context, &filename).await { if let Ok((typ, headers, _)) = split_armored_data(buf) { if typ == pgp::armor::BlockType::Message { return headers.get(crate::pgp::HEADER_SETUPCODE).cloned(); @@ -1070,21 +1110,6 @@ impl Message { self.subject = subject; } - /// Sets the file associated with a message. - /// - /// This function does not use the file or check if it exists, - /// the file will only be used when the message is prepared - /// for sending. - pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) { - if let Some(name) = Path::new(&file.to_string()).file_name() { - if let Some(name) = name.to_str() { - self.param.set(Param::Filename, name); - } - } - self.param.set(Param::File, file); - self.param.set_optional(Param::MimeType, filemime); - } - /// Sets the file associated with a message, deduplicating files with the same name. /// /// If `name` is Some, it is used as the file name @@ -1370,7 +1395,7 @@ impl Message { /// * Lack of valid signature on an e2ee message, usually for received messages. /// * Failure to decrypt an e2ee message, usually for received messages. /// * When a message could not be delivered to one or more recipients the non-delivery - /// notification text can be stored in the error status. + /// notification text can be stored in the error status. pub fn error(&self) -> Option { self.error.clone() } @@ -1604,14 +1629,12 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, & } /// Get the raw mime-headers of the given message. -/// Raw headers are saved for incoming messages -/// only if `set_config(context, "save_mime_headers", "1")` -/// was called before. +/// Raw headers are saved for large messages +/// that need a "Show full message..." +/// to see HTML part. /// -/// Returns an empty vector if there are no headers saved for the given message, -/// e.g. because of save_mime_headers is not set -/// or the message is not incoming. -pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result> { +/// Returns an empty vector if there are no headers saved for the given message. +pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result> { let (headers, compressed) = context .sql .query_row( @@ -1661,34 +1684,94 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result Result<()> { + if msg.location_id > 0 { + delete_poi_location(context, msg.location_id).await?; + } + let on_server = true; + msg.id + .trash(context, on_server) + .await + .with_context(|| format!("Unable to trash message {}", msg.id))?; + + context.emit_event(EventType::MsgDeleted { + chat_id: msg.chat_id, + msg_id: msg.id, + }); + + if msg.viewtype == Viewtype::Webxdc { + context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id }); + } + + let logging_xdc_id = context + .debug_logging + .read() + .expect("RwLock is poisoned") + .as_ref() + .map(|dl| dl.msg_id); + if let Some(id) = logging_xdc_id { + if id == msg.id { + set_debug_logging_xdc(context, None).await?; + } + } + + Ok(()) +} + +/// Do final events and jobs after batch deletion using calls to delete_msg_locally(). +/// To avoid additional database queries, collecting data is up to the caller. +pub(crate) async fn delete_msgs_locally_done( + context: &Context, + msg_ids: &[MsgId], + modified_chat_ids: HashSet, +) -> Result<()> { + for modified_chat_id in modified_chat_ids { + context.emit_msgs_changed_without_msg_id(modified_chat_id); + chatlist_events::emit_chatlist_item_changed(context, modified_chat_id); + } + if !msg_ids.is_empty() { + context.emit_msgs_changed_without_ids(); + chatlist_events::emit_chatlist_changed(context); + // Run housekeeping to delete unused blobs. + context + .set_config_internal(Config::LastHousekeeping, None) + .await?; + } + Ok(()) +} + +/// Delete messages on all devices and on IMAP. pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { - let mut modified_chat_ids = BTreeSet::new(); + delete_msgs_ex(context, msg_ids, false).await +} + +/// Delete messages on all devices, on IMAP and optionally for all chat members. +/// Deleted messages are moved to the trash chat and scheduling for deletion on IMAP. +/// When deleting messages for others, all messages must be self-sent and in the same chat. +pub async fn delete_msgs_ex( + context: &Context, + msg_ids: &[MsgId], + delete_for_all: bool, +) -> Result<()> { + let mut modified_chat_ids = HashSet::new(); + let mut deleted_rfc724_mid = Vec::new(); let mut res = Ok(()); for &msg_id in msg_ids { let msg = Message::load_from_db(context, msg_id).await?; - if msg.location_id > 0 { - delete_poi_location(context, msg.location_id).await?; - } - let on_server = true; - msg_id - .trash(context, on_server) - .await - .with_context(|| format!("Unable to trash message {msg_id}"))?; - - context.emit_event(EventType::MsgDeleted { - chat_id: msg.chat_id, - msg_id, - }); - - if msg.viewtype == Viewtype::Webxdc { - context.emit_event(EventType::WebxdcInstanceDeleted { msg_id }); - } + ensure!( + !delete_for_all || msg.from_id == ContactId::SELF, + "Can delete only own messages for others" + ); + ensure!( + !delete_for_all || msg.get_showpadlock(), + "Cannot request deletion of unencrypted message for others" + ); modified_chat_ids.insert(msg.chat_id); + deleted_rfc724_mid.push(msg.rfc724_mid.clone()); let target = context.get_delete_msgs_target().await?; let update_db = |trans: &mut rusqlite::Transaction| { @@ -1704,38 +1787,43 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { res = Err(e); continue; } - - let logging_xdc_id = context - .debug_logging - .read() - .expect("RwLock is poisoned") - .as_ref() - .map(|dl| dl.msg_id); - - if let Some(id) = logging_xdc_id { - if id == msg_id { - set_debug_logging_xdc(context, None).await?; - } - } } res?; - for modified_chat_id in modified_chat_ids { - context.emit_msgs_changed_without_msg_id(modified_chat_id); - chatlist_events::emit_chatlist_item_changed(context, modified_chat_id); - } - - if !msg_ids.is_empty() { - context.emit_msgs_changed_without_ids(); - chatlist_events::emit_chatlist_changed(context); - // Run housekeeping to delete unused blobs. + if delete_for_all { + ensure!( + modified_chat_ids.len() == 1, + "Can delete only from same chat." + ); + if let Some(chat_id) = modified_chat_ids.iter().next() { + let mut msg = Message::new_text("🚮".to_owned()); + // We don't want to send deletion requests in chats w/o encryption: + // - These are usually chats with non-DC clients who won't respect deletion requests + // anyway and display a weird trash bin message instead. + // - Deletion of world-visible unencrypted messages seems not very useful. + msg.param.set_int(Param::GuaranteeE2ee, 1); + msg.param + .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" ")); + msg.hidden = true; + send_msg(context, *chat_id, &mut msg).await?; + } + } else { context - .set_config_internal(Config::LastHousekeeping, None) + .add_sync_item(SyncData::DeleteMessages { + msgs: deleted_rfc724_mid, + }) .await?; } - // Interrupt Inbox loop to start message deletion and run housekeeping. + for &msg_id in msg_ids { + let msg = Message::load_from_db(context, msg_id).await?; + delete_msg_locally(context, &msg).await?; + } + delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?; + + // Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg(). context.scheduler.interrupt_inbox().await; + Ok(()) } @@ -2162,12 +2250,12 @@ pub enum Viewtype { /// Image message. /// If the image is a GIF and has the appropriate extension, the viewtype is auto-changed to /// `Gif` when sending the message. - /// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension - /// and retrieved via dc_msg_set_file(), dc_msg_set_dimension(). + /// File, width and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension() + /// and retrieved via dc_msg_get_file(), dc_msg_get_height(), dc_msg_get_width(). Image = 20, /// Animated GIF message. - /// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension() + /// File, width and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension() /// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height(). Gif = 21, @@ -2180,26 +2268,26 @@ pub enum Viewtype { Sticker = 23, /// Message containing an Audio file. - /// File and duration are set via dc_msg_set_file(), dc_msg_set_duration() + /// File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration() /// and retrieved via dc_msg_get_file(), dc_msg_get_duration(). Audio = 40, /// A voice message that was directly recorded by the user. /// For all other audio messages, the type #DC_MSG_AUDIO should be used. - /// File and duration are set via dc_msg_set_file(), dc_msg_set_duration() + /// File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration() /// and retrieved via dc_msg_get_file(), dc_msg_get_duration() Voice = 41, /// Video messages. /// File, width, height and durarion - /// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration() + /// are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension(), dc_msg_set_duration() /// and retrieved via /// dc_msg_get_file(), dc_msg_get_width(), /// dc_msg_get_height(), dc_msg_get_duration(). Video = 50, /// Message containing any file, eg. a PDF. - /// The file is set via dc_msg_set_file() + /// The file is set via dc_msg_set_file_and_deduplicate() /// and retrieved via dc_msg_get_file(). File = 60, diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 5a2118e1ac..7c4bb07347 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -9,7 +9,7 @@ use crate::chatlist::Chatlist; use crate::config::Config; use crate::reaction::send_reaction; use crate::receive_imf::receive_imf; -use crate::test_utils as test; +use crate::test_utils; use crate::test_utils::{TestContext, TestContextManager}; #[test] @@ -106,7 +106,7 @@ async fn test_create_webrtc_instance_noroom() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_width_height() { - let t = test::TestContext::new().await; + let t = TestContext::new().await; // test that get_width() and get_height() are returning some dimensions for images; // (as the device-chat contains a welcome-images, we check that) @@ -135,8 +135,8 @@ async fn test_get_width_height() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_quote() { - let d = test::TestContext::new().await; +async fn test_quote_basic() { + let d = TestContext::new().await; let ctx = &d.ctx; ctx.set_config(Config::ConfiguredAddr, Some("self@example.com")) @@ -144,13 +144,10 @@ async fn test_quote() { .unwrap(); let chat = d.create_chat_with_contact("", "dest@example.com").await; - let mut msg = Message::new_text("Quoted message".to_string()); - // Send message, so it gets a Message-Id. - assert!(msg.rfc724_mid.is_empty()); - let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap(); - let msg = Message::load_from_db(ctx, msg_id).await.unwrap(); + // Message has to be sent such that it gets saved to db. + chat::send_msg(ctx, chat.id, &mut msg).await.unwrap(); assert!(!msg.rfc724_mid.is_empty()); let mut msg2 = Message::new(Viewtype::Text); @@ -358,6 +355,7 @@ async fn test_markseen_msgs() -> Result<()> { let sent1 = alice.send_msg(alice_chat.id, &mut msg).await; let msg1 = bob.recv_msg(&sent1).await; let bob_chat_id = msg1.chat_id; + let mut msg = Message::new_text("this is the text!".to_string()); let sent2 = alice.send_msg(alice_chat.id, &mut msg).await; let msg2 = bob.recv_msg(&sent2).await; assert_eq!(msg1.chat_id, msg2.chat_id); @@ -380,9 +378,11 @@ async fn test_markseen_msgs() -> Result<()> { // bob sends to alice, // alice knows bob and messages appear in normal chat + let mut msg = Message::new_text("this is the text!".to_string()); let msg1 = alice .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) .await; + let mut msg = Message::new_text("this is the text!".to_string()); let msg2 = alice .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) .await; @@ -755,3 +755,62 @@ async fn test_delete_msgs_offline() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_msgs_sync() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat_id = alice.create_chat(bob).await.id; + + alice.set_config_bool(Config::SyncMsgs, true).await?; + alice2.set_config_bool(Config::SyncMsgs, true).await?; + bob.set_config_bool(Config::SyncMsgs, true).await?; + + // Alice sends a messsage and receives it on the other device + let sent1 = alice.send_text(alice_chat_id, "foo").await; + assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1); + + let msg = alice2.recv_msg(&sent1).await; + let alice2_chat_id = msg.chat_id; + assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id); + assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1); + + // Alice deletes the message; this should happen on both devices as well + delete_msgs(alice, &[sent1.sender_msg_id]).await?; + assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0); + + test_utils::sync(alice, alice2).await; + assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sanitize_filename_message() -> Result<()> { + let t = &TestContext::new().await; + let mut msg = Message::new(Viewtype::File); + + // Even if some of these characters may be valid on one platform, + // they need to be removed in case a backup is transferred to another platform + // and the UI there tries to copy the blob to a file with the original name + // before passing it to an external program. + msg.set_file_from_bytes(t, "/\\:ee.tx*T ", b"hallo", None)?; + assert_eq!(msg.get_filename().unwrap(), "ee.txT"); + + let blob = msg.param.get_file_blob(t)?.unwrap(); + assert_eq!(blob.suffix().unwrap(), "txt"); + + // The filename shouldn't be empty if there were only illegal characters: + msg.set_file_from_bytes(t, "/\\:.txt", b"hallo", None)?; + assert_eq!(msg.get_filename().unwrap(), "file.txt"); + + msg.set_file_from_bytes(t, "/\\:", b"hallo", None)?; + assert_eq!(msg.get_filename().unwrap(), "file"); + + msg.set_file_from_bytes(t, ".txt", b"hallo", None)?; + assert_eq!(msg.get_filename().unwrap(), "file.txt"); + + Ok(()) +} diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 9bb9bc73d5..af3596c052 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,28 +1,32 @@ //! # MIME message production. use std::collections::HashSet; +use std::io::Cursor; use std::path::Path; use anyhow::{bail, Context as _, Result}; use base64::Engine as _; use chrono::TimeZone; -use email::Mailbox; -use lettre_email::{Address, Header, MimeMultipartType, PartBuilder}; +use deltachat_contact_tools::sanitize_bidi_characters; +use mail_builder::headers::address::{Address, EmailAddress}; +use mail_builder::headers::HeaderType; +use mail_builder::mime::MimePart; use tokio::fs; +use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; use crate::chat::{self, Chat}; use crate::config::Config; +use crate::constants::ASM_SUBJECT; use crate::constants::{Chattype, DC_FROM_HANDSHAKE}; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::headerdef::HeaderDef; -use crate::html::new_html_mimepart; +use crate::key::DcKey; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; -use crate::mimeparser::SystemMessage; +use crate::mimeparser::{is_hidden, SystemMessage}; use crate::param::Param; use crate::peer_channels::create_iroh_header; use crate::peerstate::Peerstate; @@ -85,13 +89,15 @@ pub struct MimeFactory { /// Vector of pairs of recipient name and address that goes into the `To` field. /// /// The list of actual message recipient addresses may be different, - /// e.g. if members are hidden for broadcast lists. + /// e.g. if members are hidden for broadcast lists + /// or if the keys for some recipients are missing + /// and encrypted message cannot be sent to them. to: Vec<(String, String)>, /// Vector of pairs of past group member names and addresses. past_members: Vec<(String, String)>, - /// Timestamps of the members in the same order as in the `recipients` + /// Timestamps of the members in the same order as in the `to` /// followed by `past_members`. /// /// If this is not empty, its length @@ -102,13 +108,8 @@ pub struct MimeFactory { loaded: Loaded, 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, + /// List of Message-IDs for `References` header. + references: Vec, /// True if the message requests Message Disposition Notification /// using `Chat-Disposition-Notification-To` header. @@ -132,7 +133,6 @@ pub struct RenderedEmail { pub message: String, // pub envelope: Envelope, pub is_encrypted: bool, - pub is_gossiped: bool, pub last_added_location_id: Option, /// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted @@ -146,11 +146,15 @@ pub struct RenderedEmail { pub subject: String, } -fn new_address_with_name(name: &str, address: String) -> Address { - match name == address { - true => Address::new_mailbox(address), - false => Address::new_mailbox_with_name(name.to_string(), address), - } +fn new_address_with_name(name: &str, address: String) -> Address<'static> { + Address::new_address( + if name == address || name.is_empty() { + None + } else { + Some(name.to_string()) + }, + address, + ) } impl MimeFactory { @@ -184,9 +188,6 @@ impl MimeFactory { let mut req_mdn = false; if chat.is_self_talk() { - if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { - recipients.push(from_addr.to_string()); - } to.push((from_displayname.to_string(), from_addr.to_string())); } else if chat.is_mailing_list() { let list_post = chat @@ -295,13 +296,15 @@ impl MimeFactory { let in_reply_to: String = row.get(0)?; let references: String = row.get(1)?; - Ok(( - render_rfc724_mid_list(&in_reply_to), - render_rfc724_mid_list(&references), - )) + Ok((in_reply_to, references)) }, ) .await?; + let references: Vec = references + .trim() + .split_ascii_whitespace() + .map(|s| s.trim_start_matches('<').trim_end_matches('>').to_string()) + .collect(); let selfstatus = match attach_profile_data { true => context .get_config(Config::Selfstatus) @@ -361,7 +364,7 @@ impl MimeFactory { additional_msg_ids, }, in_reply_to: String::default(), - references: String::default(), + references: Vec::new(), req_mdn: false, last_added_location_id: None, sync_ids_to_delete: None, @@ -414,12 +417,10 @@ impl MimeFactory { fn should_force_plaintext(&self) -> bool { match &self.loaded { - Loaded::Message { chat, msg } => { - msg.param - .get_bool(Param::ForcePlaintext) - .unwrap_or_default() - || chat.typ == Chattype::Broadcast - } + Loaded::Message { msg, .. } => msg + .param + .get_bool(Param::ForcePlaintext) + .unwrap_or_default(), Loaded::Mdn { .. } => false, } } @@ -433,35 +434,6 @@ impl MimeFactory { } } - async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result { - match &self.loaded { - Loaded::Message { chat, msg } => { - let cmd = msg.param.get_cmd(); - if cmd == SystemMessage::MemberAddedToGroup - || cmd == SystemMessage::SecurejoinMessage - { - 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?; - // `gossip_period == 0` is a special case for testing, - // enabling gossip in every message. - // Otherwise "smeared timestamps" may result in the condition - // to fail even if the clock is monotonic. - if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period { - Ok(true) - } else { - Ok(false) - } - } else { - Ok(false) - } - } - Loaded::Mdn { .. } => Ok(false), - } - } - fn should_attach_profile_data(msg: &Message) -> bool { msg.param.get_cmd() != SystemMessage::SecurejoinMessage || { let step = msg.param.get(Param::Arg).unwrap_or_default(); @@ -572,26 +544,32 @@ impl MimeFactory { /// Consumes a `MimeFactory` and renders it into a message which is then stored in /// `smtp`-table to be used by the SMTP loop pub async fn render(mut self, context: &Context) -> Result { - let mut headers = Vec::

::new(); + let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); let from = new_address_with_name(&self.from_displayname, self.from_addr.clone()); - let mut to = Vec::new(); + let mut to: Vec> = Vec::new(); for (name, addr) in &self.to { - if name.is_empty() { - to.push(Address::new_mailbox(addr.clone())); - } else { - to.push(new_address_with_name(name, addr.clone())); - } + to.push(Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + )); } - let mut past_members = Vec::new(); // Contents of `Chat-Group-Past-Members` header. + let mut past_members: Vec> = Vec::new(); // Contents of `Chat-Group-Past-Members` header. for (name, addr) in &self.past_members { - if name.is_empty() { - past_members.push(Address::new_mailbox(addr.clone())); - } else { - past_members.push(new_address_with_name(name, addr.clone())); - } + past_members.push(Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + )); } debug_assert!( @@ -599,27 +577,26 @@ impl MimeFactory { || to.len() + past_members.len() == self.member_timestamps.len() ); if to.is_empty() { - to.push(Address::new_group( - "hidden-recipients".to_string(), - Vec::new(), - )); + to.push(hidden_recipients()); } // Start with Internet Message Format headers in the order of the standard example // . - let from_header = Header::new_with_value("From".into(), vec![from]).unwrap(); - headers.push(from_header.clone()); + headers.push(("From", from.into())); if let Some(sender_displayname) = &self.sender_displayname { let sender = new_address_with_name(sender_displayname, self.from_addr.clone()); - headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap()); + headers.push(("Sender", sender.into())); } - headers.push(Header::new_with_value("To".into(), to.clone()).unwrap()); + headers.push(( + "To", + mail_builder::headers::address::Address::new_list(to.clone()).into(), + )); if !past_members.is_empty() { - headers.push( - Header::new_with_value("Chat-Group-Past-Members".into(), past_members.clone()) - .unwrap(), - ); + headers.push(( + "Chat-Group-Past-Members", + mail_builder::headers::address::Address::new_list(past_members.clone()).into(), + )); } if let Loaded::Message { chat, .. } = &self.loaded { @@ -627,72 +604,75 @@ impl MimeFactory { && !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? { - headers.push( - Header::new_with_value( - "Chat-Group-Member-Timestamps".into(), + headers.push(( + "Chat-Group-Member-Timestamps", + mail_builder::headers::raw::Raw::new( self.member_timestamps .iter() .map(|ts| ts.to_string()) .collect::>() .join(" "), ) - .unwrap(), - ); + .into(), + )); } } let subject_str = self.subject_str(context).await?; - let encoded_subject = if subject_str - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ') - // We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space - // but we do not want to encode all subjects just because they contain a space. - { - subject_str.clone() - } else { - encode_words(&subject_str) - }; - headers.push(Header::new("Subject".into(), encoded_subject)); + headers.push(( + "Subject", + mail_builder::headers::text::Text::new(subject_str.to_string()).into(), + )); let date = chrono::DateTime::::from_timestamp(self.timestamp, 0) .unwrap() .to_rfc2822(); - headers.push(Header::new("Date".into(), date)); + headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); let rfc724_mid = match &self.loaded { Loaded::Message { msg, .. } => msg.rfc724_mid.clone(), Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), }; - let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid); - let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue); - headers.push(rfc724_mid_header); + headers.push(( + "Message-ID", + mail_builder::headers::message_id::MessageId::new(rfc724_mid.clone()).into(), + )); // Reply headers as in . if !self.in_reply_to.is_empty() { - headers.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone())); + headers.push(( + "In-Reply-To", + mail_builder::headers::message_id::MessageId::new(self.in_reply_to.clone()).into(), + )); } if !self.references.is_empty() { - headers.push(Header::new("References".into(), self.references.clone())); + headers.push(( + "References", + mail_builder::headers::message_id::MessageId::<'static>::new_list( + self.references.iter().map(|s| s.to_string()), + ) + .into(), + )); } // Automatic Response headers if let Loaded::Mdn { .. } = self.loaded { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-replied".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(), )); } else if context.get_config_bool(Config::Bot).await? { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-generated".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(), )); } else if let Loaded::Message { msg, .. } = &self.loaded { if msg.param.get_cmd() == SystemMessage::SecurejoinMessage { let step = msg.param.get(Param::Arg).unwrap_or_default(); if step != "vg-request" && step != "vc-request" { - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-replied".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(), )); } } @@ -700,26 +680,48 @@ impl MimeFactory { if let Loaded::Message { chat, .. } = &self.loaded { if chat.typ == Chattype::Broadcast { - let encoded_chat_name = encode_words(&chat.name); - headers.push(Header::new( - "List-ID".into(), - format!("{encoded_chat_name} <{}>", chat.grpid), + headers.push(( + "List-ID", + mail_builder::headers::text::Text::new(format!( + "{} <{}>", + chat.name, chat.grpid + )) + .into(), + )); + } + } + + if let Loaded::Message { msg, .. } = &self.loaded { + if let Some(original_rfc724_mid) = msg.param.get(Param::TextEditFor) { + headers.push(( + "Chat-Edit", + mail_builder::headers::message_id::MessageId::new( + original_rfc724_mid.to_string(), + ) + .into(), + )); + } else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) { + headers.push(( + "Chat-Delete", + mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string()) + .into(), )); } } // Non-standard headers. - headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string())); + headers.push(( + "Chat-Version", + mail_builder::headers::raw::Raw::new("1.0").into(), + )); if self.req_mdn { // we use "Chat-Disposition-Notification-To" // because replies to "Disposition-Notification-To" are weird in many cases // eg. are just freetext and/or do not follow any standard. - headers.push(Header::new( - HeaderDef::ChatDispositionNotificationTo - .get_headername() - .to_string(), - self.from_addr.clone(), + headers.push(( + "Chat-Disposition-Notification-To", + mail_builder::headers::raw::Raw::new(self.from_addr.clone()).into(), )); } @@ -732,7 +734,10 @@ impl MimeFactory { if !skip_autocrypt { // unless determined otherwise we add the Autocrypt header let aheader = encrypt_helper.get_aheader().to_string(); - headers.push(Header::new("Autocrypt".into(), aheader)); + headers.push(( + "Autocrypt", + mail_builder::headers::raw::Raw::new(aheader).into(), + )); } // Add ephemeral timer for non-MDN messages. @@ -741,68 +746,47 @@ impl MimeFactory { if let Loaded::Message { msg, .. } = &self.loaded { let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?; if let EphemeralTimer::Enabled { duration } = ephemeral_timer { - headers.push(Header::new( - "Ephemeral-Timer".to_string(), - duration.to_string(), + headers.push(( + "Ephemeral-Timer", + mail_builder::headers::raw::Raw::new(duration.to_string()).into(), )); } } - let mut is_gossiped = false; - let peerstates = self.peerstates_for_recipients(context).await?; let is_encrypted = !self.should_force_plaintext() - && encrypt_helper - .should_encrypt(context, e2ee_guaranteed, &peerstates) - .await?; + && (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?); let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { msg.param.get_cmd() == SystemMessage::SecurejoinMessage } else { false }; - let message = match &self.loaded { + let message: MimePart<'static> = match &self.loaded { Loaded::Message { msg, .. } => { let msg = msg.clone(); - let (main_part, parts) = self + let (main_part, mut parts) = self .render_message(context, &mut headers, &grpimage, is_encrypted) .await?; if parts.is_empty() { // Single part, render as regular message. main_part } else { + parts.insert(0, main_part); + // Multiple parts, render as multipart. - let part_holder = if msg.param.get_cmd() == SystemMessage::MultiDeviceSync { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=multi-device-sync".to_string(), - )) + if msg.param.get_cmd() == SystemMessage::MultiDeviceSync { + MimePart::new("multipart/report; report-type=multi-device-sync", parts) } else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=status-update".to_string(), - )) + MimePart::new("multipart/report; report-type=status-update", parts) } else { - PartBuilder::new().message_type(MimeMultipartType::Mixed) - }; - - parts - .into_iter() - .fold(part_holder.child(main_part.build()), |message, part| { - message.child(part.build()) - }) + MimePart::new("multipart/mixed", parts) + } } } Loaded::Mdn { .. } => self.render_mdn()?, }; - let get_content_type_directives_header = || { - ( - "Content-Type-Deltachat-Directives".to_string(), - "protected-headers=\"v1\"".to_string(), - ) - }; - // Split headers based on header confidentiality policy. // Headers that must go into IMF header section. @@ -811,19 +795,21 @@ impl MimeFactory { // anywhere else according to the standard. Placing headers here also allows them to be fetched // individually over IMAP without downloading the message body. This is why Chat-Version is // placed here. - let mut unprotected_headers: Vec
= Vec::new(); - - // Headers that MUST NOT go into IMF header section. - // - // These are large headers which may hit the header section size limit on the server, such as - // Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here - // that servers mess up with in the IMF header section, like Message-ID. + let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); + + // Headers that MUST NOT (only) go into IMF header section: + // - Large headers which may hit the header section size limit on the server, such as + // Chat-User-Avatar with a base64-encoded image inside. + // - Headers duplicated here that servers mess up with in the IMF header section, like + // Message-ID. + // - Nonstandard headers that should be DKIM-protected because e.g. OpenDKIM only signs + // known headers. // // The header should be hidden from MTA // by moving it either into protected part // in case of encrypted mails // or unprotected MIME preamble in case of unencrypted mails. - let mut hidden_headers: Vec
= Vec::new(); + let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); // Opportunistically protected headers. // @@ -833,17 +819,20 @@ impl MimeFactory { // // If the message is not encrypted, these headers are placed into IMF header section, so make // sure that the message will be encrypted if you place any sensitive information here. - let mut protected_headers: Vec
= Vec::new(); + let mut protected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); // MIME header . - unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into())); - for header in headers { - let header_name = header.name.to_lowercase(); + unprotected_headers.push(( + "MIME-Version", + mail_builder::headers::raw::Raw::new("1.0").into(), + )); + for header @ (original_header_name, _header_value) in &headers { + let header_name = original_header_name.to_lowercase(); if header_name == "message-id" { unprotected_headers.push(header.clone()); - hidden_headers.push(header); - } else if header_name == "chat-user-avatar" { - hidden_headers.push(header); + hidden_headers.push(header.clone()); + } else if is_hidden(&header_name) { + hidden_headers.push(header.clone()); } else if header_name == "autocrypt" && !context.get_config_bool(Config::ProtectAutocrypt).await? { @@ -854,50 +843,43 @@ impl MimeFactory { protected_headers.push(header.clone()); } - unprotected_headers.push( - Header::new_with_value( - header.name, - vec![Address::new_mailbox(self.from_addr.clone())], - ) - .unwrap(), - ); + unprotected_headers.push(( + original_header_name, + Address::new_address(None::<&'static str>, self.from_addr.clone()).into(), + )); } else if header_name == "to" { protected_headers.push(header.clone()); if is_encrypted { - unprotected_headers.push( - Header::new_with_value( - header.name, - to.clone() - .into_iter() - .map(|header| match header { - Address::Mailbox(mb) => Address::Mailbox(Mailbox { - address: mb.address, - name: None, - }), - Address::Group(name, participants) => Address::new_group( - name, - participants - .into_iter() - .map(|mb| Mailbox { - address: mb.address, - name: None, - }) - .collect(), - ), - }) - .collect::>(), - ) - .unwrap(), - ); + let mut to_without_names = to + .clone() + .into_iter() + .filter_map(|header| match header { + Address::Address(mb) => Some(Address::Address(EmailAddress { + name: None, + email: mb.email, + })), + _ => None, + }) + .collect::>(); + if to_without_names.is_empty() { + to_without_names.push(hidden_recipients()); + } + unprotected_headers.push(( + original_header_name, + Address::new_list(to_without_names).into(), + )); } else { - unprotected_headers.push(header); + unprotected_headers.push(header.clone()); } } else if is_encrypted { protected_headers.push(header.clone()); match header_name.as_str() { "subject" => { - unprotected_headers.push(Header::new(header.name, "[...]".to_string())); + unprotected_headers.push(( + "Subject", + mail_builder::headers::raw::Raw::new("[...]").into(), + )); } "date" | "in-reply-to" @@ -905,7 +887,7 @@ impl MimeFactory { | "auto-submitted" | "chat-version" | "autocrypt-setup-message" => { - unprotected_headers.push(header); + unprotected_headers.push(header.clone()); } _ => { // Other headers are removed from unprotected part. @@ -916,7 +898,7 @@ impl MimeFactory { // in case of signed-only message. // If the message is not signed, this value will not be used. protected_headers.push(header.clone()); - unprotected_headers.push(header) + unprotected_headers.push(header.clone()) } } @@ -924,48 +906,101 @@ impl MimeFactory { // Store protected headers in the inner message. let message = protected_headers .into_iter() - .fold(message, |message, header| message.header(header)); + .fold(message, |message, (header, value)| { + message.header(header, value) + }); // Add hidden headers to encrypted payload. - let mut message = hidden_headers + let mut message: MimePart<'static> = hidden_headers .into_iter() - .fold(message, |message, header| message.header(header)); + .fold(message, |message, (header, value)| { + message.header(header, value) + }); // 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? { - 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)); - is_gossiped = true; + + let gossip_period = context.get_config_i64(Config::GossipPeriod).await?; + let now = time(); + + match &self.loaded { + Loaded::Message { chat, msg } => { + if chat.typ != Chattype::Broadcast { + for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { + let Some(key) = peerstate.peek_key(verified) else { + continue; + }; + + let fingerprint = key.dc_fingerprint().hex(); + let cmd = msg.param.get_cmd(); + let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup + || cmd == SystemMessage::SecurejoinMessage + || multiple_recipients && { + let gossiped_timestamp: Option = context + .sql + .query_get_value( + "SELECT timestamp + FROM gossip_timestamp + WHERE chat_id=? AND fingerprint=?", + (chat.id, &fingerprint), + ) + .await?; + + // `gossip_period == 0` is a special case for testing, + // enabling gossip in every message. + // + // If current time is in the past compared to + // `gossiped_timestamp`, we also gossip because + // either the `gossiped_timestamp` or clock is wrong. + gossip_period == 0 + || gossiped_timestamp + .is_none_or(|ts| now >= ts + gossip_period || now < ts) + }; + + if !should_do_gossip { + continue; + } + + let header = Aheader::new( + peerstate.addr.clone(), + key.clone(), + // Autocrypt 1.1.0 specification says that + // `prefer-encrypt` attribute SHOULD NOT be included. + EncryptPreference::NoPreference, + ) + .to_string(); + + message = message.header( + "Autocrypt-Gossip", + mail_builder::headers::raw::Raw::new(header), + ); + + context + .sql + .execute( + "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) + VALUES (?, ?, ?) + ON CONFLICT (chat_id, fingerprint) + DO UPDATE SET timestamp=excluded.timestamp", + (chat.id, &fingerprint, now), + ) + .await?; + } } } + Loaded::Mdn { .. } => { + // Never gossip in MDNs. + } } // Set the appropriate Content-Type for the inner message. - let mut existing_ct = message - .get_header("Content-Type".to_string()) - .and_then(|h| h.get_value::().ok()) - .unwrap_or_else(|| "text/plain; charset=utf-8;".to_string()); - - if !existing_ct.ends_with(';') { - existing_ct += ";"; - } - let message = message.header(get_content_type_directives_header()); - - // Set the appropriate Content-Type for the outer message - let outer_message = PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(), - )); - - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!( - context, - "mimefactory: unencrypted message mime-body:\n{}", - message.clone().build().as_string(), - ); + for (h, ref mut v) in &mut message.headers { + if h == "Content-Type" { + if let mail_builder::headers::HeaderType::ContentType(ref mut ct) = v { + *ct = ct.clone().attribute("protected-headers", "v1"); + } + } } // Disable compression for SecureJoin to ensure @@ -977,32 +1012,48 @@ impl MimeFactory { } Loaded::Mdn { .. } => true, }; + + let (encryption_keyring, missing_key_addresses) = + encrypt_helper.encryption_keyring(context, verified, &peerstates)?; + + // XXX: additional newline is needed + // to pass filtermail at + // let encrypted = encrypt_helper - .encrypt(context, verified, message, peerstates, compress) - .await?; + .encrypt(context, encryption_keyring, message, compress) + .await? + + "\n"; - outer_message - .child( + // Remove recipients for which the key is missing. + if !missing_key_addresses.is_empty() { + self.recipients + .retain(|addr| !missing_key_addresses.contains(addr)); + } + + // Set the appropriate Content-Type for the outer message + MimePart::new( + "multipart/encrypted; protocol=\"application/pgp-encrypted\"", + vec![ // Autocrypt part 1 - PartBuilder::new() - .content_type(&"application/pgp-encrypted".parse::().unwrap()) - .header(("Content-Description", "PGP/MIME version identification")) - .body("Version: 1\r\n") - .build(), - ) - .child( + MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header( + "Content-Description", + mail_builder::headers::raw::Raw::new("PGP/MIME version identification"), + ), // Autocrypt part 2 - PartBuilder::new() - .content_type( - &"application/octet-stream; name=\"encrypted.asc\"" - .parse::() - .unwrap(), - ) - .header(("Content-Description", "OpenPGP encrypted message")) - .header(("Content-Disposition", "inline; filename=\"encrypted.asc\";")) - .body(encrypted) - .build(), - ) + MimePart::new( + "application/octet-stream; name=\"encrypted.asc\"", + encrypted, + ) + .header( + "Content-Description", + mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"), + ) + .header( + "Content-Disposition", + mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"), + ), + ], + ) } else if matches!(self.loaded, Loaded::Mdn { .. }) { // Never add outer multipart/mixed wrapper to MDN // as multipart/report Content-Type is used to recognize MDNs @@ -1016,68 +1067,74 @@ impl MimeFactory { } else { let message = hidden_headers .into_iter() - .fold(message, |message, header| message.header(header)); - let message = PartBuilder::new() - .message_type(MimeMultipartType::Mixed) - .child(message.build()); - let message = protected_headers + .fold(message, |message, (header, value)| { + message.header(header, value) + }); + let message = MimePart::new("multipart/mixed", vec![message]); + let mut message = protected_headers .iter() - .fold(message, |message, header| message.header(header.clone())); + .fold(message, |message, (header, value)| { + message.header(*header, value.clone()) + }); if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? { // Deduplicate unprotected headers that also are in the protected headers: let protected: HashSet<&str> = - HashSet::from_iter(protected_headers.iter().map(|h| h.name.as_str())); - unprotected_headers.retain(|h| !protected.contains(&h.name.as_str())); + HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header)); + unprotected_headers.retain(|(header, _value)| !protected.contains(header)); message } else { - let message = message.header(get_content_type_directives_header()); - let (payload, signature) = encrypt_helper.sign(context, message).await?; - PartBuilder::new() - .header(( - "Content-Type", - "multipart/signed; protocol=\"application/pgp-signature\"", - )) - .child(payload) - .child( - PartBuilder::new() - .content_type( - &"application/pgp-signature; name=\"signature.asc\"" - .parse::() - .unwrap(), - ) - .header(("Content-Description", "OpenPGP digital signature")) - .header(("Content-Disposition", "attachment; filename=\"signature\";")) - .body(signature) - .build(), - ) + for (h, ref mut v) in &mut message.headers { + if h == "Content-Type" { + if let mail_builder::headers::HeaderType::ContentType(ref mut ct) = v { + *ct = ct.clone().attribute("protected-headers", "v1"); + } + } + } + + let signature = encrypt_helper.sign(context, &message).await?; + MimePart::new( + "multipart/signed; protocol=\"application/pgp-signature\"; protected", + vec![ + message, + MimePart::new( + "application/pgp-signature; name=\"signature.asc\"", + signature, + ) + .header( + "Content-Description", + mail_builder::headers::raw::Raw::<'static>::new( + "OpenPGP digital signature", + ), + ) + .attachment("signature"), + ], + ) } }; // Store the unprotected headers on the outer message. let outer_message = unprotected_headers .into_iter() - .fold(outer_message, |message, header| message.header(header)); - - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!( - context, - "mimefactory: outgoing message mime-body:\n{}", - outer_message.clone().build().as_string(), - ); - } + .fold(outer_message, |message, (header, value)| { + message.header(header, value) + }); let MimeFactory { last_added_location_id, .. } = self; + let mut buffer = Vec::new(); + let cursor = Cursor::new(&mut buffer); + outer_message.clone().write_part(cursor).ok(); + let message = String::from_utf8_lossy(&buffer).to_string(); + Ok(RenderedEmail { - message: outer_message.build().as_string(), + message, // envelope: Envelope::new, is_encrypted, - is_gossiped, last_added_location_id, sync_ids_to_delete: self.sync_ids_to_delete, rfc724_mid, @@ -1086,7 +1143,7 @@ impl MimeFactory { } /// Returns MIME part with a `message.kml` attachment. - fn get_message_kml_part(&self) -> Option { + fn get_message_kml_part(&self) -> Option> { let Loaded::Message { msg, .. } = &self.loaded else { return None; }; @@ -1095,22 +1152,16 @@ impl MimeFactory { let longitude = msg.param.get_float(Param::SetLongitude)?; let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude); - let part = PartBuilder::new() - .content_type( - &"application/vnd.google-earth.kml+xml" - .parse::() - .unwrap(), - ) - .header(( - "Content-Disposition", - "attachment; filename=\"message.kml\"", - )) - .body(kml_file); + let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_file) + .attachment("message.kml"); Some(part) } /// Returns MIME part with a `location.kml` attachment. - async fn get_location_kml_part(&mut self, context: &Context) -> Result> { + async fn get_location_kml_part( + &mut self, + context: &Context, + ) -> Result>> { let Loaded::Message { msg, .. } = &self.loaded else { return Ok(None); }; @@ -1121,17 +1172,8 @@ impl MimeFactory { return Ok(None); }; - let part = PartBuilder::new() - .content_type( - &"application/vnd.google-earth.kml+xml" - .parse::() - .unwrap(), - ) - .header(( - "Content-Disposition", - "attachment; filename=\"location.kml\"", - )) - .body(kml_content); + let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_content) + .attachment("location.kml"); if !msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed self.last_added_location_id = Some(last_added_location_id); @@ -1139,23 +1181,13 @@ impl MimeFactory { Ok(Some(part)) } - fn add_message_text(&self, part: PartBuilder, mut text: String) -> PartBuilder { - // This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable - // encoding and thus breaking messages and signatures. It's unlikely that the reader uses a - // MUA not supporting Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for - // encrypted messages. - let part = part.header(("Content-Transfer-Encoding", "quoted-printable")); - text = quoted_printable::encode_to_str(text); - part.body(text) - } - async fn render_message( &mut self, context: &Context, - headers: &mut Vec
, + headers: &mut Vec<(&'static str, HeaderType<'static>)>, grpimage: &Option, is_encrypted: bool, - ) -> Result<(PartBuilder, Vec)> { + ) -> Result<(MimePart<'static>, Vec>)> { let Loaded::Message { chat, msg } = &self.loaded else { bail!("Attempt to render MDN as a message"); }; @@ -1172,17 +1204,31 @@ impl MimeFactory { Chattype::Broadcast => false, }; if chat.is_protected() && send_verified_headers { - headers.push(Header::new("Chat-Verified".to_string(), "1".to_string())); + headers.push(( + "Chat-Verified", + mail_builder::headers::raw::Raw::new("1").into(), + )); } if chat.typ == Chattype::Group { // Send group ID unless it is an ad hoc group that has no ID. if !chat.grpid.is_empty() { - headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone())); + headers.push(( + "Chat-Group-ID", + mail_builder::headers::raw::Raw::new(chat.grpid.clone()).into(), + )); } - let encoded = encode_words(&chat.name); - headers.push(Header::new("Chat-Group-Name".into(), encoded)); + headers.push(( + "Chat-Group-Name", + mail_builder::headers::text::Text::new(chat.name.to_string()).into(), + )); + if let Some(ts) = chat.param.get_i64(Param::GroupNameTimestamp) { + headers.push(( + "Chat-Group-Name-Timestamp", + mail_builder::headers::text::Text::new(ts.to_string()).into(), + )); + } match command { SystemMessage::MemberRemovedFromGroup => { @@ -1201,9 +1247,10 @@ impl MimeFactory { }; if !email_to_remove.is_empty() { - headers.push(Header::new( - "Chat-Group-Member-Removed".into(), - email_to_remove.into(), + headers.push(( + "Chat-Group-Member-Removed", + mail_builder::headers::raw::Raw::new(email_to_remove.to_string()) + .into(), )); } } @@ -1213,9 +1260,9 @@ impl MimeFactory { Some(stock_str::msg_add_member_remote(context, email_to_add).await); if !email_to_add.is_empty() { - headers.push(Header::new( - "Chat-Group-Member-Added".into(), - email_to_add.into(), + headers.push(( + "Chat-Group-Member-Added", + mail_builder::headers::raw::Raw::new(email_to_add.to_string()).into(), )); } if 0 != msg.param.get_int(Param::Arg2).unwrap_or_default() & DC_FROM_HANDSHAKE { @@ -1223,28 +1270,29 @@ impl MimeFactory { context, "Sending secure-join message {:?}.", "vg-member-added", ); - headers.push(Header::new( - "Secure-Join".to_string(), - "vg-member-added".to_string(), + headers.push(( + "Secure-Join", + mail_builder::headers::raw::Raw::new("vg-member-added".to_string()) + .into(), )); } } SystemMessage::GroupNameChanged => { - let old_name = msg.param.get(Param::Arg).unwrap_or_default(); - headers.push(Header::new( - "Chat-Group-Name-Changed".into(), - maybe_encode_words(old_name), + let old_name = msg.param.get(Param::Arg).unwrap_or_default().to_string(); + headers.push(( + "Chat-Group-Name-Changed", + mail_builder::headers::text::Text::new(old_name).into(), )); } SystemMessage::GroupImageChanged => { - headers.push(Header::new( - "Chat-Content".to_string(), - "group-avatar-changed".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::text::Text::new("group-avatar-changed").into(), )); if grpimage.is_none() { - headers.push(Header::new( - "Chat-Group-Avatar".to_string(), - "0".to_string(), + headers.push(( + "Chat-Group-Avatar", + mail_builder::headers::raw::Raw::new("0").into(), )); } } @@ -1254,15 +1302,15 @@ impl MimeFactory { match command { SystemMessage::LocationStreamingEnabled => { - headers.push(Header::new( - "Chat-Content".into(), - "location-streaming-enabled".into(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("location-streaming-enabled").into(), )); } SystemMessage::EphemeralTimerChanged => { - headers.push(Header::new( - "Chat-Content".to_string(), - "ephemeral-timer-changed".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("ephemeral-timer-changed").into(), )); } SystemMessage::LocationOnly @@ -1276,68 +1324,78 @@ impl MimeFactory { // Adding this header without encryption leaks some // information about the message contents, but it can // already be easily guessed from message timing and size. - headers.push(Header::new( - "Auto-Submitted".to_string(), - "auto-generated".to_string(), + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-generated").into(), )); } SystemMessage::AutocryptSetupMessage => { - headers.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into())); + headers.push(( + "Autocrypt-Setup-Message", + mail_builder::headers::raw::Raw::new("v1").into(), + )); - placeholdertext = Some(stock_str::ac_setup_msg_body(context).await); + placeholdertext = Some(ASM_SUBJECT.to_string()); } SystemMessage::SecurejoinMessage => { let step = msg.param.get(Param::Arg).unwrap_or_default(); if !step.is_empty() { info!(context, "Sending secure-join message {step:?}."); - headers.push(Header::new("Secure-Join".into(), step.into())); + headers.push(( + "Secure-Join", + mail_builder::headers::raw::Raw::new(step.to_string()).into(), + )); let param2 = msg.param.get(Param::Arg2).unwrap_or_default(); if !param2.is_empty() { - headers.push(Header::new( + headers.push(( if step == "vg-request-with-auth" || step == "vc-request-with-auth" { - "Secure-Join-Auth".into() + "Secure-Join-Auth" } else { - "Secure-Join-Invitenumber".into() + "Secure-Join-Invitenumber" }, - param2.into(), + mail_builder::headers::text::Text::new(param2.to_string()).into(), )); } let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default(); if !fingerprint.is_empty() { - headers.push(Header::new( - "Secure-Join-Fingerprint".into(), - fingerprint.into(), + headers.push(( + "Secure-Join-Fingerprint", + mail_builder::headers::raw::Raw::new(fingerprint.to_string()).into(), )); } if let Some(id) = msg.param.get(Param::Arg4) { - headers.push(Header::new("Secure-Join-Group".into(), id.into())); + headers.push(( + "Secure-Join-Group", + mail_builder::headers::raw::Raw::new(id.to_string()).into(), + )); }; } } SystemMessage::ChatProtectionEnabled => { - headers.push(Header::new( - "Chat-Content".to_string(), - "protection-enabled".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("protection-enabled").into(), )); } SystemMessage::ChatProtectionDisabled => { - headers.push(Header::new( - "Chat-Content".to_string(), - "protection-disabled".to_string(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("protection-disabled").into(), )); } SystemMessage::IrohNodeAddr => { - headers.push(Header::new( - HeaderDef::IrohNodeAddr.get_headername().to_string(), - serde_json::to_string( + headers.push(( + "Iroh-Node-Addr", + mail_builder::headers::text::Text::new(serde_json::to_string( &context .get_or_try_init_peer_channel() .await? .get_node_addr() .await?, - )?, + )?) + .into(), )); } _ => {} @@ -1348,22 +1406,31 @@ impl MimeFactory { let avatar = build_avatar_file(context, grpimage) .await .context("Cannot attach group image")?; - headers.push(Header::new( - "Chat-Group-Avatar".into(), - format!("base64:{avatar}"), + headers.push(( + "Chat-Group-Avatar", + mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(), )); } if msg.viewtype == Viewtype::Sticker { - headers.push(Header::new("Chat-Content".into(), "sticker".into())); + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("sticker").into(), + )); } else if msg.viewtype == Viewtype::VideochatInvitation { - headers.push(Header::new( - "Chat-Content".into(), - "videochat-invitation".into(), + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("videochat-invitation").into(), )); - headers.push(Header::new( - "Chat-Webrtc-Room".into(), - msg.param.get(Param::WebrtcRoom).unwrap_or_default().into(), + headers.push(( + "Chat-Webrtc-Room", + mail_builder::headers::raw::Raw::new( + msg.param + .get(Param::WebrtcRoom) + .unwrap_or_default() + .to_string(), + ) + .into(), )); } @@ -1372,12 +1439,18 @@ impl MimeFactory { || msg.viewtype == Viewtype::Video { if msg.viewtype == Viewtype::Voice { - headers.push(Header::new("Chat-Voice-Message".into(), "1".into())); + headers.push(( + "Chat-Voice-Message", + mail_builder::headers::raw::Raw::new("1").into(), + )); } let duration_ms = msg.param.get_int(Param::Duration).unwrap_or_default(); if duration_ms > 0 { let dur = duration_ms.to_string(); - headers.push(Header::new("Chat-Duration".into(), dur)); + headers.push(( + "Chat-Duration", + mail_builder::headers::raw::Raw::new(dur).into(), + )); } } @@ -1440,12 +1513,12 @@ impl MimeFactory { footer ); - let mut main_part = - PartBuilder::new().header(("Content-Type", "text/plain; charset=utf-8")); - main_part = self.add_message_text(main_part, message_text); - + let mut main_part = MimePart::new("text/plain", message_text); if is_reaction { - main_part = main_part.header(("Content-Disposition", "reaction")); + main_part = main_part.header( + "Content-Disposition", + mail_builder::headers::raw::Raw::new("reaction"), + ); } let mut parts = Vec::new(); @@ -1461,10 +1534,10 @@ impl MimeFactory { msg.param.get(Param::SendHtml).map(|s| s.to_string()) }; if let Some(html) = html { - main_part = PartBuilder::new() - .message_type(MimeMultipartType::Alternative) - .child(main_part.build()) - .child(new_html_mimepart(html).build()); + main_part = MimePart::new( + "multipart/alternative", + vec![main_part, MimePart::new("text/html", html)], + ) } } @@ -1495,7 +1568,11 @@ impl MimeFactory { let json = msg.param.get(Param::Arg).unwrap_or_default(); parts.push(context.build_status_update_part(json)); } else if msg.viewtype == Viewtype::Webxdc { - headers.push(create_iroh_header(context, msg.id).await?); + headers.push(( + "Iroh-Gossip-Topic", + mail_builder::headers::raw::Raw::new(create_iroh_header(context, msg.id).await?) + .into(), + )); if let (Some(json), _) = context .render_webxdc_status_update_object( msg.id, @@ -1512,13 +1589,16 @@ impl MimeFactory { if self.attach_selfavatar { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { - Ok(avatar) => headers.push(Header::new( - "Chat-User-Avatar".into(), - format!("base64:{avatar}"), + Ok(avatar) => headers.push(( + "Chat-User-Avatar", + mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(), )), Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), }, - None => headers.push(Header::new("Chat-User-Avatar".into(), "0".into())), + None => headers.push(( + "Chat-User-Avatar", + mail_builder::headers::raw::Raw::new("0").into(), + )), } } @@ -1526,7 +1606,7 @@ impl MimeFactory { } /// Render an MDN - fn render_mdn(&mut self) -> Result { + fn render_mdn(&mut self) -> Result> { // RFC 6522, this also requires the `report-type` parameter which is equal // to the MIME subtype of the second body part of the multipart/report // @@ -1547,21 +1627,15 @@ impl MimeFactory { bail!("Attempt to render a message as MDN"); }; - let mut message = PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=disposition-notification".to_string(), - )); - // first body part: always human-readable, always REQUIRED by RFC 6522. // untranslated to no reveal sender's language. // moreover, translations in unknown languages are confusing, and clients may not display them at all - let text_part = PartBuilder::new().header(( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - )); - let text_part = - self.add_message_text(text_part, "This is a receipt notification.\r\n".to_string()); - message = message.child(text_part.build()); + let text_part = MimePart::new("text/plain", "This is a receipt notification."); + + let mut message = MimePart::new( + "multipart/report; report-type=disposition-notification", + vec![text_part], + ); // second body part: machine-readable, always REQUIRED by RFC 6522 let message_text2 = format!( @@ -1584,33 +1658,20 @@ impl MimeFactory { + "\r\n" }; - message = message.child( - PartBuilder::new() - .content_type(&"message/disposition-notification".parse().unwrap()) - .body(message_text2 + &extension_fields) - .build(), - ); + message.add_part(MimePart::new( + "message/disposition-notification", + message_text2 + &extension_fields, + )); Ok(message) } } -/// Returns base64-encoded buffer `buf` split into 76-bytes long -/// chunks separated by CRLF. -/// -/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8) -/// says that "The encoded output stream must be represented in lines of no more than 76 characters each." -/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin. -pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String { - let base64 = base64::engine::general_purpose::STANDARD.encode(buf); - let mut chars = base64.chars(); - std::iter::repeat_with(|| chars.by_ref().take(76).collect::()) - .take_while(|s| !s.is_empty()) - .collect::>() - .join("\r\n") +fn hidden_recipients() -> Address<'static> { + Address::new_group(Some("hidden-recipients".to_string()), Vec::new()) } -async fn build_body_file(context: &Context, msg: &Message) -> Result { +async fn build_body_file(context: &Context, msg: &Message) -> Result> { let file_name = msg.get_filename().context("msg has no file")?; let suffix = Path::new(&file_name) .extension() @@ -1619,8 +1680,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result Result mtype.parse()?, + let mimetype = match msg.param.get(Param::MimeType) { + Some(mtype) => mtype.to_string(), None => { - if let Some(res) = message::guess_msgtype_from_suffix(msg) { - res.1.parse()? + if let Some((_viewtype, res)) = message::guess_msgtype_from_suffix(msg) { + res.to_string() } else { - mime::APPLICATION_OCTET_STREAM + "application/octet-stream".to_string() } } }; + let body = fs::read(blob.to_abs_path()).await?; + // create mime part, for Content-Disposition, see RFC 2183. // `Content-Disposition: attachment` seems not to make a difference to `Content-Disposition: inline` // at least on tested Thunderbird and Gma'l in 2017. // But I've heard about problems with inline and outl'k, so we just use the attachment-type until we // run into other problems ... - let cd_value = format!( - "attachment; filename=\"{}\"", - maybe_encode_words(&filename_to_send) - ); - - let body = fs::read(blob.to_abs_path()).await?; - let encoded_body = wrapped_base64_encode(&body); - - let mail = PartBuilder::new() - .content_type(&mimetype) - .header(("Content-Disposition", cd_value)) - .header(("Content-Transfer-Encoding", "base64")) - .body(encoded_body); + let mail = + MimePart::new(mimetype, body).attachment(sanitize_bidi_characters(&filename_to_send)); Ok(mail) } async fn build_avatar_file(context: &Context, path: &str) -> Result { let blob = match path.starts_with("$BLOBDIR/") { - true => BlobObject::from_name(context, path.to_string())?, + true => BlobObject::from_name(context, path)?, false => BlobObject::from_path(context, path.as_ref())?, }; let body = fs::read(blob.to_abs_path()).await?; - let encoded_body = wrapped_base64_encode(&body); + let encoded_body = base64::engine::general_purpose::STANDARD + .encode(&body) + .chars() + .enumerate() + .fold(String::new(), |mut res, (i, c)| { + if i % 78 == 77 { + res.push(' ') + } + res.push(c); + res + }); Ok(encoded_body) } @@ -1725,963 +1786,5 @@ fn render_rfc724_mid(rfc724_mid: &str) -> String { } } -fn render_rfc724_mid_list(mid_list: &str) -> String { - mid_list - .trim() - .split_ascii_whitespace() - .map(render_rfc724_mid) - .collect::>() - .join(" ") -} - -/* ****************************************************************************** - * Encode/decode header words, RFC 2047 - ******************************************************************************/ - -fn encode_words(word: &str) -> String { - encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None) -} - -fn needs_encoding(to_check: &str) -> bool { - !to_check.chars().all(|c| { - c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' || c == '%' - }) -} - -fn maybe_encode_words(words: &str) -> String { - if needs_encoding(words) { - encode_words(words) - } else { - words.to_string() - } -} - #[cfg(test)] -mod tests { - use deltachat_contact_tools::ContactAddress; - use mailparse::{addrparse_header, MailHeaderMap}; - use std::str; - - use super::*; - use crate::chat::{ - add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId, - ProtectionStatus, - }; - use crate::chatlist::Chatlist; - use crate::constants; - use crate::contact::Origin; - use crate::mimeparser::MimeMessage; - use crate::receive_imf::receive_imf; - use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; - - #[test] - fn test_render_email_address() { - let display_name = "ä space"; - let addr = "x@y.org"; - - assert!(!display_name.is_ascii()); - assert!(!display_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - - let s = format!("{}", new_address_with_name(display_name, addr.to_string())); - - println!("{s}"); - - assert_eq!(s, "=?utf-8?q?=C3=A4_space?= "); - } - - #[test] - fn test_render_email_address_noescape() { - let display_name = "a space"; - let addr = "x@y.org"; - - assert!(display_name.is_ascii()); - assert!(display_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == ' ')); - - let s = format!("{}", new_address_with_name(display_name, addr.to_string())); - - // Addresses should not be unnecessarily be encoded, see : - assert_eq!(s, "a space "); - } - - #[test] - fn test_render_email_address_duplicated_as_name() { - let addr = "x@y.org"; - let s = format!("{}", new_address_with_name(addr, addr.to_string())); - assert_eq!(s, ""); - } - - #[test] - fn test_render_rfc724_mid() { - assert_eq!( - render_rfc724_mid("kqjwle123@qlwe"), - "".to_string() - ); - assert_eq!( - render_rfc724_mid(" kqjwle123@qlwe "), - "".to_string() - ); - assert_eq!( - render_rfc724_mid(""), - "".to_string() - ); - } - - #[test] - fn test_render_rc724_mid_list() { - assert_eq!(render_rfc724_mid_list("123@q "), "<123@q>".to_string()); - assert_eq!(render_rfc724_mid_list(" 123@q "), "<123@q>".to_string()); - assert_eq!( - render_rfc724_mid_list("123@q 456@d "), - "<123@q> <456@d>".to_string() - ); - } - - #[test] - fn test_wrapped_base64_encode() { - let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - let output = - "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\ - QUFBQUFBQUFBQQ=="; - assert_eq!(wrapped_base64_encode(input), output); - } - - #[test] - fn test_needs_encoding() { - assert!(!needs_encoding("")); - assert!(!needs_encoding("foobar")); - assert!(needs_encoding(" ")); - assert!(needs_encoding("foo bar")); - } - - #[test] - fn test_maybe_encode_words() { - assert_eq!(maybe_encode_words("foobar"), "foobar"); - assert_eq!(maybe_encode_words("-_.~%"), "-_.~%"); - assert_eq!(maybe_encode_words("äöü"), "=?utf-8?b?w6TDtsO8?="); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_manually_set_subject() -> Result<()> { - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let mut msg = Message::new(Viewtype::Text); - msg.set_subject("Subjeeeeect".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let payload = sent_msg.payload(); - - assert_eq!(payload.match_indices("Subject: Subjeeeeect").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_from_mua() { - // 1.: Receive a mail from an MUA - assert_eq!( - msg_to_subject_str( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Antw: Chat: hello\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Chat: hello" - ); - - assert_eq!( - msg_to_subject_str( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Infos: 42\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Infos: 42" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_from_dc() { - // 2. Receive a message from Delta Chat - assert_eq!( - msg_to_subject_str( - 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\n\ - Subject: Chat: hello\n\ - Chat-Version: 1.0\n\ - Message-ID: <2223@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - ) - .await, - "Re: Chat: hello" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_outgoing() { - // 3. Send the first message to a new contact - let t = TestContext::new_alice().await; - - assert_eq!(first_subject_str(t).await, "Message from alice@example.org"); - - let t = TestContext::new_alice().await; - t.set_config(Config::Displayname, Some("Alice")) - .await - .unwrap(); - assert_eq!(first_subject_str(t).await, "Message from Alice"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_unicode() { - // 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result) - msg_to_subject_str( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: äääää\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - .as_bytes(), - ) - .await; - - msg_to_subject_str( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: aäääää\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n" - .as_bytes(), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_mdn() { - // 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used - let t = TestContext::new_alice().await; - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Hello, Bob\n\ - Chat-Version: 1.0\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let mut new_msg = incoming_msg_to_reply_msg( - 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\n\ - Subject: message opened\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Read receipts do not guarantee sth. was read.\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: message/disposition-notification\n\ - \n\ - Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: <2893@example.com>\n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n", &t).await; - chat::send_msg(&t, new_msg.chat_id, &mut new_msg) - .await - .unwrap(); - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - // The subject string should not be "Re: message opened" - assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mdn_create_encrypted() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - alice - .set_config(Config::Displayname, Some("Alice Exampleorg")) - .await?; - let bob = tcm.bob().await; - bob.set_config(Config::Displayname, Some("Bob Examplenet")) - .await?; - bob.set_config(Config::Selfstatus, Some("Bob Examplenet")) - .await?; - bob.set_config_bool(Config::MdnsEnabled, true).await?; - - let mut msg = Message::new(Viewtype::Text); - msg.param.set_int(Param::SkipAutocrypt, 1); - let chat_alice = alice.create_chat(&bob).await.id; - let sent = alice.send_msg(chat_alice, &mut msg).await; - - let rcvd = bob.recv_msg(&sent).await; - message::markseen_msgs(&bob, vec![rcvd.id]).await?; - let mimefactory = - MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; - let rendered_msg = mimefactory.render(&bob).await?; - - assert!(!rendered_msg.is_encrypted); - assert!(!rendered_msg.message.contains("Bob Examplenet")); - assert!(!rendered_msg.message.contains("Alice Exampleorg")); - let bob_alice_contact = bob.add_or_lookup_contact(&alice).await; - assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg"); - - let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await; - message::markseen_msgs(&bob, vec![rcvd.id]).await?; - - let mimefactory = - MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; - let rendered_msg = mimefactory.render(&bob).await?; - - // When encrypted, the MDN should be encrypted as well - assert!(rendered_msg.is_encrypted); - assert!(!rendered_msg.message.contains("Bob Examplenet")); - assert!(!rendered_msg.message.contains("Alice Exampleorg")); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_subject_in_group() -> Result<()> { - async fn send_msg_get_subject( - t: &TestContext, - group_id: ChatId, - quote: Option<&Message>, - ) -> Result { - let mut new_msg = Message::new_text("Hi".to_string()); - if let Some(q) = quote { - new_msg.set_quote(t, Some(q)).await?; - } - let sent = t.send_msg(group_id, &mut new_msg).await; - get_subject(t, sent).await - } - async fn get_subject( - t: &TestContext, - sent: crate::test_utils::SentMessage<'_>, - ) -> Result { - let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap(); - - let sent_msg = sent.load_from_db().await; - assert_eq!(parsed_subject, sent_msg.subject); - - Ok(parsed_subject) - } - - // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject - let t = TestContext::new_alice().await; - let group_id = - chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä - .await - .unwrap(); - let bob = Contact::create(&t, "", "bob@example.org").await?; - chat::add_contact_to_chat(&t, group_id, bob).await?; - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "groupname"); - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: Different subject\n\ - In-Reply-To: {}\n\ - Message-ID: <2893@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - t.get_last_msg().await.rfc724_mid - ) - .as_bytes(), - false, - ) - .await?; - let message_from_bob = t.get_last_msg().await; - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await?; - let outgoing_quoting_msg = t.get_last_msg().await; - assert_eq!(subject, "Re: Different subject"); - - let subject = send_msg_get_subject(&t, group_id, None).await?; - assert_eq!(subject, "Re: groupname"); - - let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await?; - assert_eq!(subject, "Re: Different subject"); - - chat::forward_msgs(&t, &[message_from_bob.id], group_id).await?; - let subject = get_subject(&t, t.pop_sent_msg().await).await?; - assert_eq!(subject, "Re: groupname"); - Ok(()) - } - - async fn first_subject_str(t: TestContext) -> String { - let contact_id = Contact::add_or_lookup( - &t, - "Dave", - &ContactAddress::new("dave@example.com").unwrap(), - Origin::ManuallyCreated, - ) - .await - .unwrap() - .0; - - let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); - - let mut new_msg = Message::new_text("Hi".to_string()); - new_msg.chat_id = chat_id; - chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap(); - - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - - mf.subject_str(&t).await.unwrap() - } - - // In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org - async fn msg_to_subject_str(imf_raw: &[u8]) -> String { - let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await; - - // Check that combinations of true and false reproduce the same subject_str: - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, true, false, false).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, false, true, false).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, false, true, true).await - ); - assert_eq!( - subject_str, - msg_to_subject_str_inner(imf_raw, true, true, false).await - ); - - // These two combinations are different: If `message_arrives_inbetween` is true, but - // `reply` is false, the core is actually expected to use the subject of the message - // that arrived in between. - assert_eq!( - "Re: Some other, completely unrelated subject", - msg_to_subject_str_inner(imf_raw, false, false, true).await - ); - assert_eq!( - "Re: Some other, completely unrelated subject", - msg_to_subject_str_inner(imf_raw, true, false, true).await - ); - - // We leave away the combination (true, true, true) here: - // It would mean that the original message is quoted without sending the quoting message - // out yet, then the original message is deleted, then another unrelated message arrives - // and then the message with the quote is sent out. Not very realistic. - - subject_str - } - - async fn msg_to_subject_str_inner( - imf_raw: &[u8], - delete_original_msg: bool, - reply: bool, - message_arrives_inbetween: bool, - ) -> String { - let t = TestContext::new_alice().await; - let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; - let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await; - - if delete_original_msg { - incoming_msg.id.trash(&t, false).await.unwrap(); - } - - if message_arrives_inbetween { - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Some other, completely unrelated subject\n\ - Message-ID: <3cl4@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - Some other, completely unrelated content\n", - false, - ) - .await - .unwrap(); - - let arrived_msg = t.get_last_msg().await; - assert_eq!(arrived_msg.chat_id, incoming_msg.chat_id); - } - - if reply { - new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap(); - } - - chat::send_msg(&t, new_msg.chat_id, &mut new_msg) - .await - .unwrap(); - let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); - mf.subject_str(&t).await.unwrap() - } - - // Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`. - async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message { - context - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(context, imf_raw, false).await.unwrap(); - - let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); - - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(context).await.unwrap(); - - let mut new_msg = Message::new_text("Hi".to_string()); - new_msg.chat_id = chat_id; - - new_msg - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - // This test could still be extended - async fn test_render_reply() { - let t = TestContext::new_alice().await; - let context = &t; - - let mut msg = incoming_msg_to_reply_msg( - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ - To: alice@example.org\n\ - Subject: Chat: hello\n\ - Chat-Version: 1.0\n\ - Message-ID: <2223@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n", - context, - ) - .await; - chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap(); - - let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap(); - - let recipients = mimefactory.recipients(); - assert_eq!(recipients, vec!["charlie@example.com"]); - - let rendered_msg = mimefactory.render(context).await.unwrap(); - - let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap(); - assert_eq!( - mail.headers - .iter() - .find(|h| h.get_key() == "MIME-Version") - .unwrap() - .get_value(), - "1.0" - ); - - let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None) - .await - .unwrap(); - } - - #[test] - fn test_no_empty_lines_in_header() { - // See - let to_tuples = [ - ("Nnnn", "nnn@ttttttttt.de"), - ("😀 ttttttt", "ttttttt@rrrrrr.net"), - ("dididididididi", "t@iiiiiii.org"), - ("Ttttttt", "oooooooooo@abcd.de"), - ("Mmmmm", "mmmmm@rrrrrr.net"), - ("Zzzzzz", "rrrrrrrrrrrrr@ttttttttt.net"), - ("Xyz", "qqqqqqqqqq@rrrrrr.net"), - ("", "geug@ttttttttt.de"), - ("qqqqqq", "q@iiiiiii.org"), - ("bbbb", "bbbb@iiiiiii.org"), - ("", "fsfs@iiiiiii.org"), - ("rqrqrqrqr", "rqrqr@iiiiiii.org"), - ("tttttttt", "tttttttt@iiiiiii.org"), - ("", "tttttt@rrrrrr.net"), - ] - .iter(); - let to: Vec<_> = to_tuples - .map(|(name, addr)| { - if name.is_empty() { - Address::new_mailbox(addr.to_string()) - } else { - new_address_with_name(name, addr.to_string()) - } - }) - .collect(); - - let mut message = email::MimeMessage::new_blank_message(); - message.headers.insert( - ( - "Content-Type".to_string(), - "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), - ) - .into(), - ); - message - .headers - .insert(Header::new_with_value("To".into(), to).unwrap()); - message.body = "Hi".to_string(); - - let msg = message.as_string(); - - let header_end = msg.find("Hi").unwrap(); - let headers = msg[0..header_end].trim(); - - assert!(!headers.lines().any(|l| l.trim().is_empty())); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { - // create chat with bob, set selfavatar - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await?; - t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await?; - - // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); - - let outer = payload.next().unwrap(); - let inner = payload.next().unwrap(); - let body = payload.next().unwrap(); - - assert_eq!(outer.match_indices("multipart/mixed").count(), 1); - assert_eq!(outer.match_indices("Message-ID:").count(), 1); - assert_eq!(outer.match_indices("Subject:").count(), 1); - assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); - - assert_eq!(inner.match_indices("text/plain").count(), 1); - assert_eq!(inner.match_indices("Message-ID:").count(), 1); - assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); - assert_eq!(inner.match_indices("Subject:").count(), 0); - assert_eq!(inner.match_indices("quoted-printable").count(), 1); - - assert_eq!(body.match_indices("this is the text!").count(), 1); - - // if another message is sent, that one must not contain the avatar - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); - let outer = payload.next().unwrap(); - let inner = payload.next().unwrap(); - let body = payload.next().unwrap(); - - assert_eq!(outer.match_indices("multipart/mixed").count(), 1); - assert_eq!(outer.match_indices("Message-ID:").count(), 1); - assert_eq!(outer.match_indices("Subject:").count(), 1); - assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); - - assert_eq!(inner.match_indices("text/plain").count(), 1); - assert_eq!(inner.match_indices("Message-ID:").count(), 1); - assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(inner.match_indices("Subject:").count(), 0); - assert_eq!(inner.match_indices("quoted-printable").count(), 1); - - assert_eq!(body.match_indices("this is the text!").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_unencrypted_signed() { - // create chat with bob, set selfavatar - let t = TestContext::new_alice().await; - t.set_config(Config::SignUnencrypted, Some("1")) - .await - .unwrap(); - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await.unwrap(); - t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await - .unwrap(); - - // send message to bob: that should get multipart/signed. - // `Subject:` is protected by copying it. - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("multipart/signed").count(), 1); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); - assert_eq!(part.match_indices("From:").count(), 0); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 0); - assert_eq!(part.match_indices("quoted-printable").count(), 1); - - let body = payload.next().unwrap(); - assert_eq!(body.match_indices("this is the text!").count(), 1); - - let bob = TestContext::new_bob().await; - bob.recv_msg(&sent_msg).await; - let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .unwrap() - .unwrap(); - let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); - assert!(alice_contact - .get_profile_image(&bob.ctx) - .await - .unwrap() - .is_some()); - - // if another message is sent, that one must not contain the avatar - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("multipart/signed").count(), 1); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 0); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); - assert_eq!(body.match_indices("From:").count(), 0); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 0); - assert_eq!(part.match_indices("quoted-printable").count(), 1); - - let body = payload.next().unwrap(); - assert_eq!(body.match_indices("this is the text!").count(), 1); - - bob.recv_msg(&sent_msg).await; - let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); - assert!(alice_contact - .get_profile_image(&bob.ctx) - .await - .unwrap() - .is_some()); - } - - /// Test that removed member address does not go into the `To:` field. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remove_member_bcc() -> Result<()> { - // Alice creates a group with Bob and Claire and then removes Bob. - let alice = TestContext::new_alice().await; - - let claire_addr = "claire@foo.de"; - let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "Claire", claire_addr).await?; - - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; - send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?; - - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; - let remove = alice.pop_sent_msg().await; - let remove_payload = remove.payload(); - let parsed = mailparse::parse_mail(remove_payload.as_bytes())?; - let to = parsed - .headers - .get_first_header("To") - .context("no To: header parsed")?; - let to = addrparse_header(to)?; - for to_addr in to.iter() { - match to_addr { - mailparse::MailAddr::Single(ref info) => { - // Addresses should be of existing members (Alice and Bob) and not Claire. - assert_ne!(info.addr, claire_addr); - } - mailparse::MailAddr::Group(_) => { - panic!("Group addresses are not expected here"); - } - } - } - - Ok(()) - } - - /// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_from_before_autocrypt() -> Result<()> { - // create chat with bob - let t = TestContext::new_alice().await; - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let payload = sent_msg.payload(); - - assert_eq!(payload.match_indices("Autocrypt:").count(), 1); - assert_eq!(payload.match_indices("From:").count(), 1); - - assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_protected_headers_directive() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let chat = tcm - .send_recv_accept(&alice, &bob, "alice->bob") - .await - .chat_id; - - // Now Bob can send an encrypted message to Alice. - let mut msg = Message::new(Viewtype::File); - // Long messages are truncated and MimeMessage::decoded_data is set for them. We need - // decoded_data to check presence of the necessary headers. - msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1)); - msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?; - let sent = bob.send_msg(chat, &mut msg).await; - assert!(msg.get_showpadlock()); - assert!(sent.payload.contains("\r\nSubject: [...]\r\n")); - - let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?; - let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n"); - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("Subject:").count(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_remove_self() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let first_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; - alice.send_text(first_group, "Hi! I created a group.").await; - remove_contact_from_chat(alice, first_group, ContactId::SELF).await?; - alice.pop_sent_msg().await; - - let second_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) - .await; - let sent = alice - .send_text(second_group, "Hi! I created another group.") - .await; - - println!("{}", sent.payload); - let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None) - .await - .unwrap(); - assert_eq!( - mime_message.get_header(HeaderDef::ChatGroupPastMembers), - None - ); - assert_eq!( - mime_message.chat_group_member_timestamps().unwrap().len(), - 1 // There is a timestamp for Bob, not for Alice - ); - - Ok(()) - } -} +mod mimefactory_tests; diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs new file mode 100644 index 0000000000..22306ddebe --- /dev/null +++ b/src/mimefactory/mimefactory_tests.rs @@ -0,0 +1,924 @@ +use deltachat_contact_tools::ContactAddress; +use mail_builder::headers::Header; +use mailparse::{addrparse_header, MailHeaderMap}; +use std::str; + +use super::*; +use crate::chat::{ + self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId, + ProtectionStatus, +}; +use crate::chatlist::Chatlist; +use crate::constants; +use crate::contact::Origin; +use crate::headerdef::HeaderDef; +use crate::mimeparser::MimeMessage; +use crate::receive_imf::receive_imf; +use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; + +fn render_email_address(display_name: &str, addr: &str) -> String { + let mut output = Vec::::new(); + new_address_with_name(display_name, addr.to_string()) + .unwrap_address() + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() +} + +#[test] +fn test_render_email_address() { + let display_name = "ä space"; + let addr = "x@y.org"; + + assert!(!display_name.is_ascii()); + assert!(!display_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == ' ')); + + let s = render_email_address(display_name, addr); + + println!("{s}"); + + assert_eq!(s, r#""=?utf-8?B?w6Qgc3BhY2U=?=" "#); +} + +#[test] +fn test_render_email_address_noescape() { + let display_name = "a space"; + let addr = "x@y.org"; + + assert!(display_name.is_ascii()); + assert!(display_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == ' ')); + + let s = render_email_address(display_name, addr); + + // Addresses should not be unnecessarily be encoded, see : + assert_eq!(s, r#""a space" "#); +} + +#[test] +fn test_render_email_address_duplicated_as_name() { + let addr = "x@y.org"; + let s = render_email_address(addr, addr); + assert_eq!(s, ""); +} + +#[test] +fn test_render_rfc724_mid() { + assert_eq!( + render_rfc724_mid("kqjwle123@qlwe"), + "".to_string() + ); + assert_eq!( + render_rfc724_mid(" kqjwle123@qlwe "), + "".to_string() + ); + assert_eq!( + render_rfc724_mid(""), + "".to_string() + ); +} + +fn render_header_text(text: &str) -> String { + let mut output = Vec::::new(); + mail_builder::headers::text::Text::new(text.to_string()) + .write_header(&mut output, 0) + .unwrap(); + + String::from_utf8(output).unwrap() +} + +#[test] +fn test_header_encoding() { + assert_eq!(render_header_text("foobar"), "foobar\r\n"); + assert_eq!(render_header_text("-_.~%"), "-_.~%\r\n"); + assert_eq!(render_header_text("äöü"), "=?utf-8?B?w6TDtsO8?=\r\n"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_manually_set_subject() -> Result<()> { + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let mut msg = Message::new(Viewtype::Text); + msg.set_subject("Subjeeeeect".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let payload = sent_msg.payload(); + + assert_eq!(payload.match_indices("Subject: Subjeeeeect").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_from_mua() { + // 1.: Receive a mail from an MUA + assert_eq!( + msg_to_subject_str( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Antw: Chat: hello\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Chat: hello" + ); + + assert_eq!( + msg_to_subject_str( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Infos: 42\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Infos: 42" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_from_dc() { + // 2. Receive a message from Delta Chat + assert_eq!( + msg_to_subject_str( + 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\n\ + Subject: Chat: hello\n\ + Chat-Version: 1.0\n\ + Message-ID: <2223@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + ) + .await, + "Re: Chat: hello" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_outgoing() { + // 3. Send the first message to a new contact + let t = TestContext::new_alice().await; + + assert_eq!(first_subject_str(t).await, "Message from alice@example.org"); + + let t = TestContext::new_alice().await; + t.set_config(Config::Displayname, Some("Alice")) + .await + .unwrap(); + assert_eq!(first_subject_str(t).await, "Message from Alice"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_unicode() { + // 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result) + msg_to_subject_str( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: äääää\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + .as_bytes(), + ) + .await; + + msg_to_subject_str( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: aäääää\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n" + .as_bytes(), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_mdn() { + // 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used + let t = TestContext::new_alice().await; + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Hello, Bob\n\ + Chat-Version: 1.0\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let mut new_msg = incoming_msg_to_reply_msg( + 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\n\ + Subject: message opened\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + Chat-Version: 1.0\n\ + Message-ID: \n\ + Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: text/plain; charset=utf-8\n\ + \n\ + Read receipts do not guarantee sth. was read.\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: message/disposition-notification\n\ + \n\ + Reporting-UA: Delta Chat 1.28.0\n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ + Original-Message-ID: <2893@example.com>\n\ + Disposition: manual-action/MDN-sent-automatically; displayed\n\ + \n", &t).await; + chat::send_msg(&t, new_msg.chat_id, &mut new_msg) + .await + .unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + // The subject string should not be "Re: message opened" + assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdn_create_encrypted() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + alice + .set_config(Config::Displayname, Some("Alice Exampleorg")) + .await?; + let bob = tcm.bob().await; + bob.set_config(Config::Displayname, Some("Bob Examplenet")) + .await?; + bob.set_config(Config::Selfstatus, Some("Bob Examplenet")) + .await?; + bob.set_config_bool(Config::MdnsEnabled, true).await?; + + let mut msg = Message::new(Viewtype::Text); + msg.param.set_int(Param::SkipAutocrypt, 1); + let chat_alice = alice.create_chat(&bob).await.id; + let sent = alice.send_msg(chat_alice, &mut msg).await; + + let rcvd = bob.recv_msg(&sent).await; + message::markseen_msgs(&bob, vec![rcvd.id]).await?; + let mimefactory = + MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; + let rendered_msg = mimefactory.render(&bob).await?; + + assert!(!rendered_msg.is_encrypted); + assert!(!rendered_msg.message.contains("Bob Examplenet")); + assert!(!rendered_msg.message.contains("Alice Exampleorg")); + let bob_alice_contact = bob.add_or_lookup_contact(&alice).await; + assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg"); + + let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await; + message::markseen_msgs(&bob, vec![rcvd.id]).await?; + + let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; + let rendered_msg = mimefactory.render(&bob).await?; + + // When encrypted, the MDN should be encrypted as well + assert!(rendered_msg.is_encrypted); + assert!(!rendered_msg.message.contains("Bob Examplenet")); + assert!(!rendered_msg.message.contains("Alice Exampleorg")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_subject_in_group() -> Result<()> { + async fn send_msg_get_subject( + t: &TestContext, + group_id: ChatId, + quote: Option<&Message>, + ) -> Result { + let mut new_msg = Message::new_text("Hi".to_string()); + if let Some(q) = quote { + new_msg.set_quote(t, Some(q)).await?; + } + let sent = t.send_msg(group_id, &mut new_msg).await; + get_subject(t, sent).await + } + async fn get_subject( + t: &TestContext, + sent: crate::test_utils::SentMessage<'_>, + ) -> Result { + let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap(); + + let sent_msg = sent.load_from_db().await; + assert_eq!(parsed_subject, sent_msg.subject); + + Ok(parsed_subject) + } + + // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject + let t = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä + .await + .unwrap(); + let bob_contact_id = t.add_or_lookup_contact_id(&bob).await; + chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?; + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "groupname"); + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: Different subject\n\ + In-Reply-To: {}\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + t.get_last_msg().await.rfc724_mid + ) + .as_bytes(), + false, + ) + .await?; + let message_from_bob = t.get_last_msg().await; + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await?; + let outgoing_quoting_msg = t.get_last_msg().await; + assert_eq!(subject, "Re: Different subject"); + + let subject = send_msg_get_subject(&t, group_id, None).await?; + assert_eq!(subject, "Re: groupname"); + + let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await?; + assert_eq!(subject, "Re: Different subject"); + + chat::forward_msgs(&t, &[message_from_bob.id], group_id).await?; + let subject = get_subject(&t, t.pop_sent_msg().await).await?; + assert_eq!(subject, "Re: groupname"); + Ok(()) +} + +async fn first_subject_str(t: TestContext) -> String { + let contact_id = Contact::add_or_lookup( + &t, + "Dave", + &ContactAddress::new("dave@example.com").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap() + .0; + + let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); + + let mut new_msg = Message::new_text("Hi".to_string()); + new_msg.chat_id = chat_id; + chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap(); + + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + + mf.subject_str(&t).await.unwrap() +} + +// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org +async fn msg_to_subject_str(imf_raw: &[u8]) -> String { + let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await; + + // Check that combinations of true and false reproduce the same subject_str: + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, false, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, true).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, true, false).await + ); + + // These two combinations are different: If `message_arrives_inbetween` is true, but + // `reply` is false, the core is actually expected to use the subject of the message + // that arrived in between. + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, false, false, true).await + ); + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, true, false, true).await + ); + + // We leave away the combination (true, true, true) here: + // It would mean that the original message is quoted without sending the quoting message + // out yet, then the original message is deleted, then another unrelated message arrives + // and then the message with the quote is sent out. Not very realistic. + + subject_str +} + +async fn msg_to_subject_str_inner( + imf_raw: &[u8], + delete_original_msg: bool, + reply: bool, + message_arrives_inbetween: bool, +) -> String { + let t = TestContext::new_alice().await; + let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; + let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await; + + if delete_original_msg { + incoming_msg.id.trash(&t, false).await.unwrap(); + } + + if message_arrives_inbetween { + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Some other, completely unrelated subject\n\ + Message-ID: <3cl4@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + Some other, completely unrelated content\n", + false, + ) + .await + .unwrap(); + + let arrived_msg = t.get_last_msg().await; + assert_eq!(arrived_msg.chat_id, incoming_msg.chat_id); + } + + if reply { + new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap(); + } + + chat::send_msg(&t, new_msg.chat_id, &mut new_msg) + .await + .unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); + mf.subject_str(&t).await.unwrap() +} + +// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`. +async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message { + context + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(context, imf_raw, false).await.unwrap(); + + let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); + + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(context).await.unwrap(); + + let mut new_msg = Message::new_text("Hi".to_string()); + new_msg.chat_id = chat_id; + + new_msg +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +// This test could still be extended +async fn test_render_reply() { + let t = TestContext::new_alice().await; + let context = &t; + + let mut msg = incoming_msg_to_reply_msg( + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Charlie \n\ + To: alice@example.org\n\ + Subject: Chat: hello\n\ + Chat-Version: 1.0\n\ + Message-ID: <2223@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + context, + ) + .await; + chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap(); + + let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap(); + + let recipients = mimefactory.recipients(); + assert_eq!(recipients, vec!["charlie@example.com"]); + + let rendered_msg = mimefactory.render(context).await.unwrap(); + + let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap(); + assert_eq!( + mail.headers + .iter() + .find(|h| h.get_key() == "MIME-Version") + .unwrap() + .get_value(), + "1.0" + ); + + let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None) + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { + // create chat with bob, set selfavatar + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + + // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); + assert_eq!(inner.match_indices("Subject:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + + // if another message is sent, that one must not contain the avatar + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(inner.match_indices("Subject:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_group_avatar_unencrypted() -> anyhow::Result<()> { + let t = &TestContext::new_alice().await; + let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group") + .await + .unwrap(); + let bob = Contact::create(t, "", "bob@example.org").await?; + chat::add_contact_to_chat(t, group_id, bob).await?; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?; + + // Send message to bob: that should get multipart/mixed because of the avatar moved to inner header. + let mut msg = Message::new_text("this is the text!".to_string()); + let sent_msg = t.send_msg(group_id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-Group-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-Group-Avatar:").count(), 1); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_unencrypted_signed() { + // create chat with bob, set selfavatar + let t = TestContext::new_alice().await; + t.set_config(Config::SignUnencrypted, Some("1")) + .await + .unwrap(); + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await.unwrap(); + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await + .unwrap(); + + // send message to bob: that should get multipart/signed. + // `Subject:` is protected by copying it. + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("multipart/signed").count(), 1); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!(part.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 0); + + let body = payload.next().unwrap(); + assert_eq!(body.match_indices("this is the text!").count(), 1); + + let bob = TestContext::new_bob().await; + bob.recv_msg(&sent_msg).await; + let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .unwrap() + .unwrap(); + let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); + assert!(alice_contact + .get_profile_image(&bob.ctx) + .await + .unwrap() + .is_some()); + + // if another message is sent, that one must not contain the avatar + let mut msg = Message::new_text("this is the text!".to_string()); + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("multipart/signed").count(), 1); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 1); + assert_eq!(part.match_indices("Autocrypt:").count(), 0); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!(body.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 0); + + let body = payload.next().unwrap(); + assert_eq!(body.match_indices("this is the text!").count(), 1); + + bob.recv_msg(&sent_msg).await; + let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); + assert!(alice_contact + .get_profile_image(&bob.ctx) + .await + .unwrap() + .is_some()); +} + +/// Test that removed member address does not go into the `To:` field. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remove_member_bcc() -> Result<()> { + let mut tcm = TestContextManager::new(); + + // Alice creates a group with Bob and Claire and then removes Bob. + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + let bob_id = alice.add_or_lookup_contact_id(bob).await; + let charlie_id = alice.add_or_lookup_contact_id(charlie).await; + let charlie_contact = Contact::get_by_id(alice, charlie_id).await?; + let charlie_addr = charlie_contact.get_addr(); + + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?; + add_contact_to_chat(alice, alice_chat_id, bob_id).await?; + add_contact_to_chat(alice, alice_chat_id, charlie_id).await?; + send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?; + + remove_contact_from_chat(alice, alice_chat_id, charlie_id).await?; + let remove = alice.pop_sent_msg().await; + let remove_payload = remove.payload(); + let parsed = mailparse::parse_mail(remove_payload.as_bytes())?; + let to = parsed + .headers + .get_first_header("To") + .context("no To: header parsed")?; + let to = addrparse_header(to)?; + for to_addr in to.iter() { + match to_addr { + mailparse::MailAddr::Single(ref info) => { + // Addresses should be of existing members (Alice and Bob) and not Charlie. + assert_ne!(info.addr, charlie_addr); + } + mailparse::MailAddr::Group(_) => { + panic!("Group addresses are not expected here"); + } + } + } + + Ok(()) +} + +/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_from_before_autocrypt() -> Result<()> { + // create chat with bob + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new_text("this is the text!".to_string()); + + let sent_msg = t.send_msg(chat.id, &mut msg).await; + let payload = sent_msg.payload(); + + assert_eq!(payload.match_indices("Autocrypt:").count(), 1); + assert_eq!(payload.match_indices("From:").count(), 1); + + assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protected_headers_directive() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat = tcm + .send_recv_accept(&alice, &bob, "alice->bob") + .await + .chat_id; + + // Now Bob can send an encrypted message to Alice. + let mut msg = Message::new(Viewtype::File); + // Long messages are truncated and MimeMessage::decoded_data is set for them. We need + // decoded_data to check presence of the necessary headers. + msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1)); + msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?; + let sent = bob.send_msg(chat, &mut msg).await; + assert!(msg.get_showpadlock()); + assert!(sent.payload.contains("\r\nSubject: [...]\r\n")); + + let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?; + let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n"); + let part = payload.next().unwrap(); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); + assert_eq!(part.match_indices("Subject:").count(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_remove_self() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let first_group = alice + .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) + .await; + alice.send_text(first_group, "Hi! I created a group.").await; + remove_contact_from_chat(alice, first_group, ContactId::SELF).await?; + alice.pop_sent_msg().await; + + let second_group = alice + .create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob]) + .await; + let sent = alice + .send_text(second_group, "Hi! I created another group.") + .await; + + println!("{}", sent.payload); + let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None) + .await + .unwrap(); + assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers)); + assert_eq!( + mime_message.chat_group_member_timestamps().unwrap().len(), + 1 // There is a timestamp for Bob, not for Alice + ); + + Ok(()) +} + +/// Regression test: mimefactory should never create an empty to header, +/// also not if the Selftalk parameter is missing +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_empty_to_header() -> Result<()> { + let alice = &TestContext::new_alice().await; + let mut self_chat = alice.get_self_chat().await; + self_chat.param.remove(Param::Selftalk); + self_chat.update_param(alice).await?; + + let payload = alice.send_text(self_chat.id, "Hi").await.payload; + assert!( + // It would be equally fine if the payload contained `To: alice@example.org` or similar, + // as long as it's a valid header + payload.contains("To: \"hidden-recipients\": ;"), + "Payload doesn't contain correct To: header: {payload}" + ); + + Ok(()) +} diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 03c7cd40b1..f6c9c13279 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -10,8 +10,8 @@ use anyhow::{bail, Context as _, Result}; use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters}; use deltachat_derive::{FromSql, ToSql}; use format_flowed::unformat_flowed; -use lettre_email::mime::Mime; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; +use mime::Mime; use crate::aheader::{Aheader, EncryptPreference}; use crate::authres::handle_authres; @@ -57,6 +57,10 @@ pub(crate) struct MimeMessage { /// Message headers. headers: HashMap, + #[cfg(test)] + /// Names of removed (ignored) headers. Used by `header_exists()` needed for tests. + headers_removed: HashSet, + /// List of addresses from the `To` and `Cc` headers. /// /// Addresses are normalized and lowercase. @@ -236,6 +240,7 @@ impl MimeMessage { let mut hop_info = parse_receive_headers(&mail.get_headers()); let mut headers = Default::default(); + let mut headers_removed = HashSet::::new(); let mut recipients = Default::default(); let mut past_members = Default::default(); let mut from = Default::default(); @@ -253,6 +258,12 @@ impl MimeMessage { &mut chat_disposition_notification_to, &mail.headers, ); + headers.retain(|k, _| { + !is_hidden(k) || { + headers_removed.insert(k.clone()); + false + } + }); // Parse hidden headers. let mimetype = mail.ctype.mimetype.parse::()?; @@ -263,7 +274,7 @@ impl MimeMessage { // messages are shown as unencrypted anyway. timestamp_sent = - Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd); + Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd); MimeMessage::merge_headers( context, &mut headers, @@ -287,11 +298,7 @@ impl MimeMessage { if let Some(part) = part.subparts.first() { 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") - { + if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" { headers.insert(key.to_string(), field.get_value()); } } @@ -301,9 +308,11 @@ impl MimeMessage { // 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()) - { + if let Some(microsoft_message_id) = remove_header( + &mut headers, + HeaderDef::XMicrosoftOriginalMessageId.get_headername(), + &mut headers_removed, + ) { headers.insert( HeaderDef::MessageId.get_headername().to_string(), microsoft_message_id, @@ -312,7 +321,7 @@ impl MimeMessage { // 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); + Self::remove_secured_headers(&mut headers, &mut headers_removed); let mut from = from.context("No from in message")?; let private_keyring = load_self_secret_keyring(context).await?; @@ -348,6 +357,13 @@ impl MimeMessage { } decrypted_msg = Some(msg); + + timestamp_sent = Self::get_timestamp_sent( + &decrypted_mail.headers, + timestamp_sent, + timestamp_rcvd, + ); + if let Some(protected_aheader_value) = decrypted_mail .headers .get_header_value(HeaderDef::Autocrypt) @@ -419,22 +435,7 @@ impl MimeMessage { content }); if let (Ok(mail), true) = (mail, encrypted) { - timestamp_sent = - Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd); if !signatures.is_empty() { - // Handle any gossip headers if the mail was encrypted. See section - // "3.6 Key Gossip" of - // but only if the mail was correctly signed. Probably it's ok to not require - // encryption here, but let's follow the standard. - let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_keys = update_gossip_peerstates( - context, - timestamp_sent, - &from.addr, - &recipients, - gossip_headers, - ) - .await?; // Remove unsigned opportunistically protected headers from messages considered // Autocrypt-encrypted / displayed with padlock. // For "Subject" see . @@ -443,13 +444,17 @@ impl MimeMessage { HeaderDef::ChatGroupId, HeaderDef::ChatGroupName, HeaderDef::ChatGroupNameChanged, + HeaderDef::ChatGroupNameTimestamp, HeaderDef::ChatGroupAvatar, HeaderDef::ChatGroupMemberRemoved, HeaderDef::ChatGroupMemberAdded, HeaderDef::ChatGroupMemberTimestamps, HeaderDef::ChatGroupPastMembers, + HeaderDef::ChatDelete, + HeaderDef::ChatEdit, + HeaderDef::ChatUserAvatar, ] { - headers.remove(h.get_headername()); + remove_header(&mut headers, h.get_headername(), &mut headers_removed); } } @@ -471,6 +476,22 @@ impl MimeMessage { &mail.headers, ); + if !signatures.is_empty() { + // Handle any gossip headers if the mail was encrypted. See section + // "3.6 Key Gossip" of + // but only if the mail was correctly signed. Probably it's ok to not require + // encryption here, but let's follow the standard. + let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); + gossiped_keys = update_gossip_peerstates( + context, + timestamp_sent, + &from.addr, + &recipients, + gossip_headers, + ) + .await?; + } + if let Some(inner_from) = inner_from { if !addr_cmp(&inner_from.addr, &from.addr) { // There is a From: header in the encrypted @@ -497,7 +518,7 @@ impl MimeMessage { } } if signatures.is_empty() { - Self::remove_secured_headers(&mut headers); + Self::remove_secured_headers(&mut headers, &mut headers_removed); // If it is not a read receipt, degrade encryption. if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) { @@ -521,6 +542,9 @@ impl MimeMessage { let mut parser = MimeMessage { parts: Vec::new(), headers, + #[cfg(test)] + headers_removed, + recipients, past_members, list_post, @@ -922,6 +946,16 @@ impl MimeMessage { .map(|s| s.as_str()) } + #[cfg(test)] + /// Returns whether the header exists in any part of the parsed message. + /// + /// Use this to check for header absense. Header presense should be checked using + /// `get_header(...).is_some()` as it also checks that the header isn't ignored. + pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool { + let hname = headerdef.get_headername(); + self.headers.contains_key(hname) || self.headers_removed.contains(hname) + } + /// Returns `Chat-Group-ID` header value if it is a valid group ID. pub fn get_chat_group_id(&self) -> Option<&str> { self.get_header(HeaderDef::ChatGroupId) @@ -1517,14 +1551,17 @@ impl MimeMessage { .and_then(|msgid| parse_message_id(msgid).ok()) } - fn remove_secured_headers(headers: &mut HashMap) { - headers.remove("secure-join-fingerprint"); - headers.remove("secure-join-auth"); - headers.remove("chat-verified"); - headers.remove("autocrypt-gossip"); + fn remove_secured_headers( + headers: &mut HashMap, + removed: &mut HashSet, + ) { + remove_header(headers, "secure-join-fingerprint", removed); + remove_header(headers, "secure-join-auth", removed); + remove_header(headers, "chat-verified", removed); + remove_header(headers, "autocrypt-gossip", removed); // Secure-Join is secured unless it is an initial "vc-request"/"vg-request". - if let Some(secure_join) = headers.remove("secure-join") { + if let Some(secure_join) = remove_header(headers, "secure-join", removed) { if secure_join == "vc-request" || secure_join == "vg-request" { headers.insert("secure-join".to_string(), secure_join); } @@ -1852,6 +1889,19 @@ impl MimeMessage { } } +fn remove_header( + headers: &mut HashMap, + key: &str, + removed: &mut HashSet, +) -> Option { + if let Some((k, v)) = headers.remove_entry(key) { + removed.insert(k); + Some(v) + } else { + None + } +} + /// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates. /// Params: /// from: The address which sent the message currently being parsed @@ -1981,6 +2031,14 @@ fn is_known(key: &str) -> bool { ) } +/// Returns if the header is hidden and must be ignored in the IMF section. +pub(crate) fn is_hidden(key: &str) -> bool { + matches!( + key, + "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit" + ) +} + /// Parsed MIME part. #[derive(Debug, Default, Clone)] pub struct Part { @@ -2322,7 +2380,7 @@ async fn handle_ndn( let aggregated_error = message .error .as_ref() - .map(|err| format!("{}\n\n{}", err, err_msg)); + .map(|err| format!("{err}\n\n{err_msg}")); set_msg_failed( context, &mut message, diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs index ac738545b1..2e7c29dbff 100644 --- a/src/mimeparser/mimeparser_tests.rs +++ b/src/mimeparser/mimeparser_tests.rs @@ -1,10 +1,11 @@ use mailparse::ParsedMail; +use std::mem; use super::*; use crate::{ chat, chatlist::Chatlist, - constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}, + constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}, message::{MessageState, MessengerMessage}, receive_imf::receive_imf, test_utils::{TestContext, TestContextManager}, @@ -312,6 +313,19 @@ fn test_mailparse_content_type() { ); } +/// Test to reproduce +/// . +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailparse_0_16_0_panic() { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../../test-data/message/mailparse-0.16.0-panic.eml"); + + // There should be an error, but no panic. + assert!(MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .is_err()); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_first_addr() { let context = TestContext::new().await; @@ -785,7 +799,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== // Make sure the file is there even though the html is wrong: let param = &message.parts[0].param; - let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap(); + let blob: BlobObject = param.get_file_blob(&t).unwrap().unwrap(); let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap(); let size = f.metadata().await.unwrap().len(); assert_eq!(size, 154); @@ -1798,22 +1812,202 @@ async fn test_take_last_header() { ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_protect_autocrypt() -> Result<()> { +async fn test_protect_autocrypt(enabled: bool) -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; + let chat = alice.create_chat(bob).await; alice - .set_config_bool(Config::ProtectAutocrypt, true) + .set_config_bool(Config::ProtectAutocrypt, enabled) .await?; - bob.set_config_bool(Config::ProtectAutocrypt, true).await?; + let sent = alice.send_text(chat.id, "Hello!").await; + assert_eq!(sent.payload().contains("Autocrypt: "), !enabled); + let msg = bob.recv_msg(&sent).await; + assert_eq!(msg.get_showpadlock(), true); - let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; - assert_eq!(msg.get_showpadlock(), false); + Ok(()) +} - let msg = tcm.send_recv(bob, alice, "Hi!").await; - assert_eq!(msg.get_showpadlock(), true); +/// Tests that if `protect_autocrypt` is enabled, +/// `Autocrypt` header does not appear in the outer headers +/// of encrypted messages. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protect_autocrypt_enabled() -> Result<()> { + test_protect_autocrypt(true).await +} + +/// Tests that if `protect_autocrypt` is disabled, +/// `Autocrypt` header appears in the outer headers +/// of encrypted messages. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protect_autocrypt_false() -> Result<()> { + test_protect_autocrypt(false).await +} + +/// Tests that CRLF before MIME boundary +/// is not treated as the part body. +/// +/// RFC 2046 explicitly says that +/// "The CRLF preceding the boundary delimiter line is conceptually attached +/// to the boundary so that it is possible to have a part that does not end +/// with a CRLF (line break). Body parts that must be considered to end with +/// line breaks, therefore, must have two CRLFs preceding the boundary delimiter +/// line, the first of which is part of the preceding body part, +/// and the second of which is part of the encapsulation boundary." +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mimeparser_trailing_newlines() { + let context = TestContext::new_alice().await; + + // Example from + // with a `Content-Disposition` headers added to turn files + // into attachments. + let raw = b"From: Nathaniel Borenstein \r +To: Ned Freed \r +Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST)\r +Subject: Sample message\r +MIME-Version: 1.0\r +Content-type: multipart/mixed; boundary=\"simple boundary\"\r +\r +This is the preamble. It is to be ignored, though it\r +is a handy place for composition agents to include an\r +explanatory note to non-MIME conformant readers.\r +\r +--simple boundary\r +Content-Disposition: attachment; filename=\"file1.txt\"\r +\r +This is implicitly typed plain US-ASCII text.\r +It does NOT end with a linebreak.\r +--simple boundary\r +Content-type: text/plain; charset=us-ascii\r +Content-Disposition: attachment; filename=\"file2.txt\"\r +\r +This is explicitly typed plain US-ASCII text.\r +It DOES end with a linebreak.\r +\r +--simple boundary--\r +\r +This is the epilogue. It is also to be ignored."; + + let mimeparser = MimeMessage::from_bytes(&context, &raw[..], None) + .await + .unwrap(); + + assert_eq!(mimeparser.parts.len(), 2); + + assert_eq!(mimeparser.parts[0].typ, Viewtype::File); + let blob: BlobObject = mimeparser.parts[0] + .param + .get_file_blob(&context) + .unwrap() + .unwrap(); + assert_eq!( + tokio::fs::read_to_string(blob.to_abs_path()).await.unwrap(), + "This is implicitly typed plain US-ASCII text.\r\nIt does NOT end with a linebreak." + ); + + assert_eq!(mimeparser.parts[1].typ, Viewtype::File); + let blob: BlobObject = mimeparser.parts[1] + .param + .get_file_blob(&context) + .unwrap() + .unwrap(); + assert_eq!( + tokio::fs::read_to_string(blob.to_abs_path()).await.unwrap(), + "This is explicitly typed plain US-ASCII text.\r\nIt DOES end with a linebreak.\r\n" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_hidden_message_id() { + let t = &TestContext::new().await; + let raw = br#"Message-ID: bar@example.org +Date: Sun, 08 Dec 2019 23:12:55 +0000 +To: +From: +Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw" + + +--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw +Message-ID: foo@example.org +Content-Type: text/plain; charset=utf-8 + +Message with a correct Message-ID hidden header + +--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw-- +"#; + + let message = MimeMessage::from_bytes(t, &raw[..], None).await.unwrap(); + assert_eq!(message.get_rfc724_mid().unwrap(), "foo@example.org"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_edit_imf_header() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_email_chat(bob).await; + + // Alice sends a message, then sends an invalid edit request. + let sent1 = alice.send_text(alice_chat.id, "foo").await; + let alice_msg = sent1.load_from_db().await; + assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1); + + chat::send_edit_request(alice, alice_msg.id, "bar".to_string()).await?; + let mut sent2 = alice.pop_sent_msg().await; + let mut s0 = String::new(); + let mut s1 = String::new(); + for l in sent2.payload.lines() { + if l.starts_with("Chat-Edit:") { + s1 += l; + s1 += "\n"; + continue; + } + s0 += l; + s0 += "\n"; + if l.starts_with("Message-ID:") && s1.is_empty() { + s1 = mem::take(&mut s0); + } + } + sent2.payload = s1 + &s0; + + // Bob receives both messages, the edit request with "Chat-Edit" in IMF headers is + // received as text message. + let bob_msg = bob.recv_msg(&sent1).await; + assert_eq!(bob_msg.text, "foo"); + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1); + let bob_msg = bob.recv_msg(&sent2).await; + assert_eq!(bob_msg.text, constants::EDITED_PREFIX.to_string() + "bar"); + assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2); + + Ok(()) +} + +/// Tests that timestamp of signed but not encrypted message is protected. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_protected_date() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + alice.set_config(Config::SignUnencrypted, Some("1")).await?; + + let alice_chat = alice.create_email_chat(bob).await; + let alice_msg_id = chat::send_text_msg(alice, alice_chat.id, "Hello!".to_string()).await?; + let alice_msg = Message::load_from_db(alice, alice_msg_id).await?; + assert_eq!(alice_msg.get_showpadlock(), false); + + let mut sent_msg = alice.pop_sent_msg().await; + sent_msg.payload = sent_msg.payload.replacen( + "Date:", + "Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)\r\nX-Not-Date:", + 1, + ); + let bob_msg = bob.recv_msg(&sent_msg).await; + assert_eq!(alice_msg.get_text(), bob_msg.get_text()); + // Timestamp that the sender has put into the message + // should always be displayed as is on the receiver. + assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp()); Ok(()) } diff --git a/src/net/dns.rs b/src/net/dns.rs index 5a24a11117..9af780ed77 100644 --- a/src/net/dns.rs +++ b/src/net/dns.rs @@ -44,13 +44,13 @@ use anyhow::{Context as _, Result}; use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::str::FromStr; +use std::sync::LazyLock; use tokio::net::lookup_host; use tokio::time::timeout; use super::load_connection_timestamp; use crate::context::Context; use crate::tools::time; -use once_cell::sync::Lazy; /// Inserts entry into DNS cache /// or updates existing one with a new timestamp. @@ -90,8 +90,8 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> { /// /// and /// . -static LOOKUP_HOST_CACHE: Lazy>>> = - Lazy::new(Default::default); +static LOOKUP_HOST_CACHE: LazyLock>>> = + LazyLock::new(Default::default); /// Wrapper for `lookup_host` that returns IP addresses. async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result> { @@ -229,7 +229,7 @@ pub(crate) async fn update_connect_timestamp( /// /// See and /// for reasons. -static DNS_PRELOAD: Lazy>> = Lazy::new(|| { +static DNS_PRELOAD: LazyLock>> = LazyLock::new(|| { HashMap::from([ ( "mail.sangham.net", diff --git a/src/net/http.rs b/src/net/http.rs index b72a3729bf..e863e8f992 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -167,7 +167,7 @@ async fn http_cache_get(context: &Context, url: &str) -> Result stale_timestamp; - let blob_object = BlobObject::from_name(context, blob_name)?; + let blob_object = BlobObject::from_name(context, &blob_name)?; let blob_abs_path = blob_object.to_abs_path(); let blob = match fs::read(blob_abs_path) .await @@ -253,7 +253,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result { .headers() .get_all("location") .iter() - .last() + .next_back() .ok_or_else(|| anyhow!("Redirection doesn't have a target location"))? .to_str()?; info!(context, "Following redirect to {}", header); diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 30e2fd510e..fb76979724 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -154,18 +154,21 @@ impl Socks5Config { } } +/// Configuration for the proxy through which all traffic +/// (except for iroh p2p connections) +/// will be sent. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProxyConfig { - // HTTP proxy. + /// HTTP proxy. Http(HttpConfig), - // HTTPS proxy. + /// HTTPS proxy. Https(HttpConfig), - // SOCKS5 proxy. + /// SOCKS5 proxy. Socks5(Socks5Config), - // Shadowsocks proxy. + /// Shadowsocks proxy. Shadowsocks(ShadowsocksConfig), } @@ -246,7 +249,7 @@ where impl ProxyConfig { /// Creates a new proxy configuration by parsing given proxy URL. - pub(crate) fn from_url(url: &str) -> Result { + pub fn from_url(url: &str) -> Result { let url = Url::parse(url).context("Cannot parse proxy URL")?; match url.scheme() { "http" => { @@ -305,7 +308,7 @@ impl ProxyConfig { /// /// This function can be used to normalize proxy URL /// by parsing it and serializing back. - pub(crate) fn to_url(&self) -> String { + pub fn to_url(&self) -> String { match self { Self::Http(http_config) => http_config.to_url("http"), Self::Https(http_config) => http_config.to_url("https"), @@ -391,7 +394,7 @@ impl ProxyConfig { /// If `load_dns_cache` is true, loads cached DNS resolution results. /// Use this only if the connection is going to be protected with TLS checks. - pub async fn connect( + pub(crate) async fn connect( &self, context: &Context, target_host: &str, diff --git a/src/param.rs b/src/param.rs index 6adf2b052d..9500778a27 100644 --- a/src/param.rs +++ b/src/param.rs @@ -3,6 +3,7 @@ use std::fmt; use std::path::PathBuf; use std::str; +use anyhow::ensure; use anyhow::{bail, Error, Result}; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; @@ -205,7 +206,18 @@ pub enum Param { /// For messages: Whether [crate::message::Viewtype::Sticker] should be forced. ForceSticker = b'X', - // 'L' was defined as ProtectionSettingsTimestamp for Chats, however, never used in production. + + /// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete. + DeleteRequestFor = b'M', + + /// For messages: Message is a text edit message. the value of this parameter is the rfc724_mid of the original message. + TextEditFor = b'I', + + /// For messages: Message text was edited. + IsEdited = b'L', + + /// For info messages: Contact ID in added or removed to a group. + ContactAddedRemoved = b'5', } /// An object for handling key=value parameter lists. @@ -290,6 +302,9 @@ impl Params { /// Set the given key to the passed in value. pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self { + if key == Param::File { + debug_assert!(value.to_string().starts_with("$BLOBDIR/")); + } self.inner.insert(key, value.to_string()); self } @@ -352,59 +367,20 @@ impl Params { self.get(key).and_then(|s| s.parse().ok()) } - /// Gets the given parameter and parse as [ParamsFile]. - /// - /// See also [Params::get_blob] and [Params::get_path] which may - /// be more convenient. - pub fn get_file<'a>(&self, key: Param, context: &'a Context) -> Result>> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - ParamsFile::from_param(context, val).map(Some) - } - - /// Gets the parameter and returns a [BlobObject] for it. - /// - /// This parses the parameter value as a [ParamsFile] and than - /// tries to return a [BlobObject] for that file. If the file is - /// not yet a valid blob, one will be created by copying the file. - /// - /// Note that in the [ParamsFile::FsPath] case the blob can be - /// created without copying if the path already refers to a valid - /// blob. If so a [BlobObject] will be returned. - pub async fn get_blob<'a>( - &self, - key: Param, - context: &'a Context, - ) -> Result>> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - let file = ParamsFile::from_param(context, val)?; - let blob = match file { - ParamsFile::FsPath(path) => BlobObject::new_from_path(context, &path).await?, - ParamsFile::Blob(blob) => blob, + /// Returns a [BlobObject] for the [Param::File] parameter. + pub fn get_file_blob<'a>(&self, context: &'a Context) -> Result>> { + let Some(val) = self.get(Param::File) else { + return Ok(None); }; + ensure!(val.starts_with("$BLOBDIR/")); + let blob = BlobObject::from_name(context, val)?; Ok(Some(blob)) } - /// Gets the parameter and returns a [PathBuf] for it. - /// - /// This parses the parameter value as a [ParamsFile] and returns - /// a [PathBuf] to the file. - pub fn get_path(&self, key: Param, context: &Context) -> Result> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - let file = ParamsFile::from_param(context, val)?; - let path = match file { - ParamsFile::FsPath(path) => path, - ParamsFile::Blob(blob) => blob.to_abs_path(), - }; - Ok(Some(path)) + /// Returns a [PathBuf] for the [Param::File] parameter. + pub fn get_file_path(&self, context: &Context) -> Result> { + let blob = self.get_file_blob(context)?; + Ok(blob.map(|p| p.to_abs_path())) } /// Set the given parameter to the passed in `i32`. @@ -426,48 +402,18 @@ impl Params { } } -/// The value contained in [Param::File]. -/// -/// Because the only way to construct this object is from a valid -/// UTF-8 string it is always safe to convert the value contained -/// within the [ParamsFile::FsPath] back to a [String] or [&str]. -/// Despite the type itself does not guarantee this. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParamsFile<'a> { - FsPath(PathBuf), - Blob(BlobObject<'a>), -} - -impl<'a> ParamsFile<'a> { - /// Parse the [Param::File] value into an object. - /// - /// If the value was stored into the [Params] correctly this - /// should not fail. - pub fn from_param(context: &'a Context, src: &str) -> Result> { - let param = match src.starts_with("$BLOBDIR/") { - true => ParamsFile::Blob(BlobObject::from_name(context, src.to_string())?), - false => ParamsFile::FsPath(PathBuf::from(src)), - }; - Ok(param) - } -} - #[cfg(test)] mod tests { - use std::path::Path; use std::str::FromStr; - use tokio::fs; - use super::*; - use crate::test_utils::TestContext; #[test] fn test_dc_param() { - let mut p1: Params = "a=1\nf=2\nc=3".parse().unwrap(); + let mut p1: Params = "a=1\nw=2\nc=3".parse().unwrap(); assert_eq!(p1.get_int(Param::Forwarded), Some(1)); - assert_eq!(p1.get_int(Param::File), Some(2)); + assert_eq!(p1.get_int(Param::Width), Some(2)); assert_eq!(p1.get_int(Param::Height), None); assert!(!p1.exists(Param::Height)); @@ -478,13 +424,13 @@ mod tests { let mut p1 = Params::new(); p1.set(Param::Forwarded, "foo") - .set_int(Param::File, 2) + .set_int(Param::Width, 2) .remove(Param::GuaranteeE2ee) .set_int(Param::Duration, 4); - assert_eq!(p1.to_string(), "a=foo\nd=4\nf=2"); + assert_eq!(p1.to_string(), "a=foo\nd=4\nw=2"); - p1.remove(Param::File); + p1.remove(Param::Width); assert_eq!(p1.to_string(), "a=foo\nd=4",); assert_eq!(p1.len(), 2); @@ -506,56 +452,6 @@ mod tests { assert_eq!(params.to_string().parse::().unwrap(), params); } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_file_fs_path() { - let t = TestContext::new().await; - if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t, "/foo/bar/baz").unwrap() { - assert_eq!(p, Path::new("/foo/bar/baz")); - } else { - panic!("Wrong enum variant"); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_file_blob() { - let t = TestContext::new().await; - if let ParamsFile::Blob(b) = ParamsFile::from_param(&t, "$BLOBDIR/foo").unwrap() { - assert_eq!(b.as_name(), "$BLOBDIR/foo"); - } else { - panic!("Wrong enum variant"); - } - } - - // Tests for Params::get_file(), Params::get_path() and Params::get_blob(). - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_get_fileparam() { - let t = TestContext::new().await; - let fname = t.dir.path().join("foo"); - let mut p = Params::new(); - p.set(Param::File, fname.to_str().unwrap()); - - let file = p.get_file(Param::File, &t).unwrap().unwrap(); - assert_eq!(file, ParamsFile::FsPath(fname.clone())); - - let path: PathBuf = p.get_path(Param::File, &t).unwrap().unwrap(); - assert_eq!(path, fname); - - fs::write(fname, b"boo").await.unwrap(); - let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap(); - assert!(blob.as_file_name().starts_with("foo")); - - // Blob in blobdir, expect blob. - let bar_path = t.get_blobdir().join("bar"); - p.set(Param::File, bar_path.to_str().unwrap()); - let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap(); - assert_eq!(blob, BlobObject::from_name(&t, "bar".to_string()).unwrap()); - - p.remove(Param::File); - assert!(p.get_file(Param::File, &t).unwrap().is_none()); - assert!(p.get_path(Param::File, &t).unwrap().is_none()); - assert!(p.get_blob(Param::File, &t).await.unwrap().is_none()); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_params_unknown_key() -> Result<()> { // 'Z' is used as a key that is known to be unused; these keys should be ignored silently by definition. diff --git a/src/peer_channels.rs b/src/peer_channels.rs index 158f3785de..26250f7c81 100644 --- a/src/peer_channels.rs +++ b/src/peer_channels.rs @@ -25,9 +25,8 @@ use anyhow::{anyhow, bail, Context as _, Result}; use data_encoding::BASE32_NOPAD; -use email::Header; use futures_lite::StreamExt; -use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMap, RelayMode, RelayUrl, SecretKey}; +use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey}; use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN}; use iroh_gossip::proto::TopicId; use parking_lot::Mutex; @@ -40,7 +39,6 @@ use url::Url; use crate::chat::send_msg; use crate::config::Config; use crate::context::Context; -use crate::headerdef::HeaderDef; use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; use crate::EventType; @@ -247,7 +245,7 @@ impl Context { .as_ref() .and_then(|conf| conf.iroh_relay.clone()) { - RelayMode::Custom(RelayMap::from_url(RelayUrl::from(relay_url))) + RelayMode::Custom(RelayUrl::from(relay_url).into()) } else { // FIXME: this should be RelayMode::Disabled instead. // Currently using default relays because otherwise Rust tests fail. @@ -255,6 +253,7 @@ impl Context { }; let endpoint = Endpoint::builder() + .tls_x509() // For compatibility with iroh <0.34.0 .secret_key(secret_key) .alpns(vec![GOSSIP_ALPN.to_vec()]) .relay_mode(relay_mode) @@ -273,8 +272,7 @@ impl Context { let router = iroh::protocol::Router::builder(endpoint) .accept(GOSSIP_ALPN, gossip.clone()) - .spawn() - .await?; + .spawn(); Ok(Iroh { router, @@ -496,14 +494,11 @@ fn create_random_topic() -> TopicId { /// Creates `Iroh-Gossip-Header` with a new random topic /// and stores the topic for the message. -pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result
{ +pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result { let topic = create_random_topic(); insert_topic_stub(ctx, msg_id, topic).await?; let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase(); - Ok(Header::new( - HeaderDef::IrohGossipTopic.get_headername().to_string(), - topic_string, - )) + Ok(topic_string) } async fn subscribe_loop( diff --git a/src/peerstate.rs b/src/peerstate.rs index 9d3f526e11..daef7beadb 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -405,28 +405,6 @@ impl Peerstate { }; } - /// Returns the contents of the `Autocrypt-Gossip` header for outgoing messages. - pub fn render_gossip_header(&self, verified: bool) -> Option { - if let Some(key) = self.peek_key(verified) { - let header = Aheader::new( - self.addr.clone(), - key.clone(), // TODO: avoid cloning - // Autocrypt 1.1.0 specification says that - // `prefer-encrypt` attribute SHOULD NOT be included, - // but we include it anyway to propagate encryption - // preference to new members in group chats. - if self.last_seen_autocrypt > 0 { - self.prefer_encrypt - } else { - EncryptPreference::NoPreference - }, - ); - Some(header.to_string()) - } else { - None - } - } - /// Converts the peerstate into the contact public key. /// /// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key. @@ -676,14 +654,7 @@ impl Peerstate { let lastmsg = Message::load_from_db(context, *msg_id).await?; lastmsg.timestamp_sort } else { - context - .sql - .query_get_value( - "SELECT created_timestamp FROM chats WHERE id=?;", - (chat_id,), - ) - .await? - .unwrap_or(0) + chat_id.created_timestamp(context).await? }; if let PeerstateChange::Aeap(new_addr) = &change { @@ -755,6 +726,7 @@ impl Peerstate { Some(timestamp), None, None, + None, ) .await?; } diff --git a/src/pgp.rs b/src/pgp.rs index 804d520c09..de363a7d46 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -1,7 +1,6 @@ //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). use std::collections::{BTreeMap, HashSet}; -use std::io; use std::io::Cursor; use anyhow::{bail, Context as _, Result}; @@ -14,11 +13,10 @@ use pgp::composed::{ use pgp::crypto::ecc_curve::ECCCurve; use pgp::crypto::hash::HashAlgorithm; use pgp::crypto::sym::SymmetricKeyAlgorithm; -use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey}; -use rand::{thread_rng, CryptoRng, Rng}; +use pgp::types::{CompressionAlgorithm, PublicKeyTrait, StringToKey}; +use rand::thread_rng; use tokio::runtime::Handle; -use crate::constants::KeyGenType; use crate::key::{DcKey, Fingerprint}; #[cfg(test)] @@ -32,95 +30,6 @@ const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AE /// Preferred cryptographic hash. const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256; -/// A wrapper for rPGP public key types -#[derive(Debug)] -enum SignedPublicKeyOrSubkey<'a> { - Key(&'a SignedPublicKey), - Subkey(&'a SignedPublicSubKey), -} - -impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> { - fn version(&self) -> pgp::types::KeyVersion { - match self { - Self::Key(k) => k.version(), - Self::Subkey(k) => k.version(), - } - } - - fn fingerprint(&self) -> pgp::types::Fingerprint { - match self { - Self::Key(k) => k.fingerprint(), - Self::Subkey(k) => k.fingerprint(), - } - } - - fn key_id(&self) -> pgp::types::KeyId { - match self { - Self::Key(k) => k.key_id(), - Self::Subkey(k) => k.key_id(), - } - } - - fn algorithm(&self) -> pgp::crypto::public_key::PublicKeyAlgorithm { - match self { - Self::Key(k) => k.algorithm(), - Self::Subkey(k) => k.algorithm(), - } - } - - fn created_at(&self) -> &chrono::DateTime { - match self { - Self::Key(k) => k.created_at(), - Self::Subkey(k) => k.created_at(), - } - } - - fn expiration(&self) -> Option { - match self { - Self::Key(k) => k.expiration(), - Self::Subkey(k) => k.expiration(), - } - } - - fn verify_signature( - &self, - hash: HashAlgorithm, - data: &[u8], - sig: &SignatureBytes, - ) -> pgp::errors::Result<()> { - match self { - Self::Key(k) => k.verify_signature(hash, data, sig), - Self::Subkey(k) => k.verify_signature(hash, data, sig), - } - } - - fn encrypt( - &self, - rng: R, - plain: &[u8], - typ: pgp::types::EskType, - ) -> pgp::errors::Result { - match self { - Self::Key(k) => k.encrypt(rng, plain, typ), - Self::Subkey(k) => k.encrypt(rng, plain, typ), - } - } - - fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> { - match self { - Self::Key(k) => k.serialize_for_hashing(writer), - Self::Subkey(k) => k.serialize_for_hashing(writer), - } - } - - fn public_params(&self) -> &pgp::types::PublicParams { - match self { - Self::Key(k) => k.public_params(), - Self::Subkey(k) => k.public_params(), - } - } -} - /// Split data from PGP Armored Data as defined in . /// /// Returns (type, headers, base64 encoded body). @@ -181,15 +90,9 @@ impl KeyPair { /// /// 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 { - let (signing_key_type, encryption_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::EdDSALegacy, - PgpKeyType::ECDH(ECCCurve::Curve25519), - ), - }; +pub(crate) fn create_keypair(addr: EmailAddress) -> Result { + let signing_key_type = PgpKeyType::EdDSALegacy; + let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519); let user_id = format!("<{addr}>"); let key_params = SecretKeyParamsBuilder::default() @@ -243,28 +146,15 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res Ok(key_pair) } -/// Select public key or subkey to use for encryption. +/// Selects a subkey of the public key to use for encryption. /// -/// First, tries to use subkeys. If none of the subkeys are suitable -/// for encryption, tries to use primary key. Returns `None` if the public -/// key cannot be used for encryption. +/// Returns `None` if the public key cannot be used for encryption. /// /// TODO: take key flags and expiration dates into account -fn select_pk_for_encryption(key: &SignedPublicKey) -> Option { +fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> { key.public_subkeys .iter() .find(|subkey| subkey.is_encryption_key()) - .map_or_else( - || { - // No usable subkey found, try primary key - if key.is_encryption_key() { - Some(SignedPublicKeyOrSubkey::Key(key)) - } else { - None - } - }, - |subkey| Some(SignedPublicKeyOrSubkey::Subkey(subkey)), - ) } /// Encrypts `plain` text using `public_keys_for_encryption` @@ -279,11 +169,10 @@ pub async fn pk_encrypt( Handle::current() .spawn_blocking(move || { - let pkeys: Vec = public_keys_for_encryption + let pkeys: Vec<&SignedPublicSubKey> = public_keys_for_encryption .iter() .filter_map(select_pk_for_encryption) .collect(); - let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect(); let mut rng = thread_rng(); @@ -294,13 +183,9 @@ pub async fn pk_encrypt( } else { signed_msg }; - compressed_msg.encrypt_to_keys_seipdv1( - &mut rng, - SYMMETRIC_KEY_ALGORITHM, - &pkeys_refs, - )? + compressed_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)? } else { - lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)? + lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)? }; let encoded_msg = encrypted_msg.to_armored_string(Default::default())?; @@ -379,13 +264,6 @@ pub fn pk_validate( let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0; - // Remove trailing CRLF before the delimiter. - // According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of - // OpenPGP signature calculation. - let content = content - .get(..content.len().saturating_sub(2)) - .context("index is out of range")?; - for pkey in public_keys_for_validation { if standalone_signature.verify(pkey, content).is_ok() { let fp = pkey.dc_fingerprint(); @@ -438,7 +316,7 @@ pub async fn symm_decrypt( #[cfg(test)] mod tests { - use once_cell::sync::Lazy; + use std::sync::LazyLock; use tokio::sync::OnceCell; use super::*; @@ -485,16 +363,8 @@ mod tests { #[test] fn test_create_keypair() { - let keypair0 = create_keypair( - EmailAddress::new("foo@bar.de").unwrap(), - KeyGenType::Default, - ) - .unwrap(); - let keypair1 = create_keypair( - EmailAddress::new("two@zwo.de").unwrap(), - KeyGenType::Default, - ) - .unwrap(); + let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap(); + let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap(); assert_ne!(keypair0.public, keypair1.public); } @@ -524,7 +394,7 @@ mod tests { static CLEARTEXT: &[u8] = b"This is a test"; /// Initialised [TestKeys] for tests. - static KEYS: Lazy = Lazy::new(TestKeys::new); + static KEYS: LazyLock = LazyLock::new(TestKeys::new); static CTEXT_SIGNED: OnceCell = OnceCell::const_new(); static CTEXT_UNSIGNED: OnceCell = OnceCell::const_new(); diff --git a/src/plaintext.rs b/src/plaintext.rs index d59ac80a57..349b020218 100644 --- a/src/plaintext.rs +++ b/src/plaintext.rs @@ -1,6 +1,6 @@ //! Handle plain text together with some attributes. -use once_cell::sync::Lazy; +use std::sync::LazyLock; use crate::simplify::remove_message_footer; @@ -25,10 +25,10 @@ impl PlainText { /// Convert plain text to HTML. /// The function handles quotes, links, fixed and floating text paragraphs. pub fn to_html(&self) -> String { - static LINKIFY_MAIL_RE: Lazy = - Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap()); + static LINKIFY_MAIL_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap()); - static LINKIFY_URL_RE: Lazy = Lazy::new(|| { + static LINKIFY_URL_RE: LazyLock = LazyLock::new(|| { regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap() }); diff --git a/src/provider.rs b/src/provider.rs index e812ec54a5..8a060d6d9a 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -4,7 +4,9 @@ pub(crate) mod data; use anyhow::Result; use deltachat_contact_tools::EmailAddress; +use hickory_resolver::name_server::TokioConnectionProvider; use hickory_resolver::{config, Resolver, TokioResolver}; +use serde::{Deserialize, Serialize}; use crate::config::Config; use crate::context::Context; @@ -37,7 +39,19 @@ pub enum Protocol { } /// Socket security. -#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)] +#[derive( + Debug, + Default, + Display, + PartialEq, + Eq, + Copy, + Clone, + FromPrimitive, + ToPrimitive, + Serialize, + Deserialize, +)] #[repr(u8)] pub enum Socket { /// Unspecified socket security, select automatically. @@ -166,14 +180,14 @@ impl ProviderOptions { /// This does not work at least on some Androids, therefore we fallback /// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`. fn get_resolver() -> Result { - if let Ok(resolver) = Resolver::tokio_from_system_conf() { - return Ok(resolver); + if let Ok(resolver) = TokioResolver::builder_tokio() { + return Ok(resolver.build()); } - let resolver = Resolver::tokio( + let resolver = Resolver::builder_with_config( config::ResolverConfig::default(), - config::ResolverOpts::default(), + TokioConnectionProvider::default(), ); - Ok(resolver) + Ok(resolver.build()) } /// Returns provider for the given an e-mail address. diff --git a/src/provider/data.rs b/src/provider/data.rs index 4d52ac2c05..269137e79e 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -8,7 +8,7 @@ use crate::provider::{ }; use std::collections::HashMap; -use once_cell::sync::Lazy; +use std::sync::LazyLock; // 163.md: 163.com static P_163: Provider = Provider { @@ -2406,84 +2406,85 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [ ("zoho.com", &P_ZOHO), ]; -pub(crate) static PROVIDER_IDS: Lazy> = Lazy::new(|| { - HashMap::from([ - ("163", &P_163), - ("aktivix.org", &P_AKTIVIX_ORG), - ("aliyun", &P_ALIYUN), - ("aol", &P_AOL), - ("arcor.de", &P_ARCOR_DE), - ("autistici.org", &P_AUTISTICI_ORG), - ("blindzeln.org", &P_BLINDZELN_ORG), - ("bluewin.ch", &P_BLUEWIN_CH), - ("buzon.uy", &P_BUZON_UY), - ("chello.at", &P_CHELLO_AT), - ("comcast", &P_COMCAST), - ("daleth.cafe", &P_DALETH_CAFE), - ("dismail.de", &P_DISMAIL_DE), - ("disroot", &P_DISROOT), - ("e.email", &P_E_EMAIL), - ("espiv.net", &P_ESPIV_NET), - ("example.com", &P_EXAMPLE_COM), - ("fastmail", &P_FASTMAIL), - ("firemail.de", &P_FIREMAIL_DE), - ("five.chat", &P_FIVE_CHAT), - ("freenet.de", &P_FREENET_DE), - ("gmail", &P_GMAIL), - ("gmx.net", &P_GMX_NET), - ("hermes.radio", &P_HERMES_RADIO), - ("hey.com", &P_HEY_COM), - ("i.ua", &P_I_UA), - ("i3.net", &P_I3_NET), - ("icloud", &P_ICLOUD), - ("infomaniak.com", &P_INFOMANIAK_COM), - ("kolst.com", &P_KOLST_COM), - ("kontent.com", &P_KONTENT_COM), - ("mail.com", &P_MAIL_COM), - ("mail.de", &P_MAIL_DE), - ("mail.ru", &P_MAIL_RU), - ("mail2tor", &P_MAIL2TOR), - ("mailbox.org", &P_MAILBOX_ORG), - ("mailo.com", &P_MAILO_COM), - ("mehl.cloud", &P_MEHL_CLOUD), - ("mehl.store", &P_MEHL_STORE), - ("migadu", &P_MIGADU), - ("nauta.cu", &P_NAUTA_CU), - ("naver", &P_NAVER), - ("nine.testrun.org", &P_NINE_TESTRUN_ORG), - ("nubo.coop", &P_NUBO_COOP), - ("outlook.com", &P_OUTLOOK_COM), - ("ouvaton.coop", &P_OUVATON_COOP), - ("posteo", &P_POSTEO), - ("protonmail", &P_PROTONMAIL), - ("purelymail.com", &P_PURELYMAIL_COM), - ("qq", &P_QQ), - ("rambler.ru", &P_RAMBLER_RU), - ("riseup.net", &P_RISEUP_NET), - ("rogers.com", &P_ROGERS_COM), - ("sonic", &P_SONIC), - ("stinpriza.net", &P_STINPRIZA_NET), - ("systemausfall.org", &P_SYSTEMAUSFALL_ORG), - ("systemli.org", &P_SYSTEMLI_ORG), - ("t-online", &P_T_ONLINE), - ("testrun", &P_TESTRUN), - ("tiscali.it", &P_TISCALI_IT), - ("tutanota", &P_TUTANOTA), - ("ukr.net", &P_UKR_NET), - ("undernet.uy", &P_UNDERNET_UY), - ("vfemail", &P_VFEMAIL), - ("vivaldi", &P_VIVALDI), - ("vk.com", &P_VK_COM), - ("vodafone.de", &P_VODAFONE_DE), - ("web.de", &P_WEB_DE), - ("wkpb.de", &P_WKPB_DE), - ("yahoo", &P_YAHOO), - ("yandex.ru", &P_YANDEX_RU), - ("yggmail", &P_YGGMAIL), - ("ziggo.nl", &P_ZIGGO_NL), - ("zoho", &P_ZOHO), - ]) -}); - -pub static _PROVIDER_UPDATED: Lazy = - Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap()); +pub(crate) static PROVIDER_IDS: LazyLock> = + LazyLock::new(|| { + HashMap::from([ + ("163", &P_163), + ("aktivix.org", &P_AKTIVIX_ORG), + ("aliyun", &P_ALIYUN), + ("aol", &P_AOL), + ("arcor.de", &P_ARCOR_DE), + ("autistici.org", &P_AUTISTICI_ORG), + ("blindzeln.org", &P_BLINDZELN_ORG), + ("bluewin.ch", &P_BLUEWIN_CH), + ("buzon.uy", &P_BUZON_UY), + ("chello.at", &P_CHELLO_AT), + ("comcast", &P_COMCAST), + ("daleth.cafe", &P_DALETH_CAFE), + ("dismail.de", &P_DISMAIL_DE), + ("disroot", &P_DISROOT), + ("e.email", &P_E_EMAIL), + ("espiv.net", &P_ESPIV_NET), + ("example.com", &P_EXAMPLE_COM), + ("fastmail", &P_FASTMAIL), + ("firemail.de", &P_FIREMAIL_DE), + ("five.chat", &P_FIVE_CHAT), + ("freenet.de", &P_FREENET_DE), + ("gmail", &P_GMAIL), + ("gmx.net", &P_GMX_NET), + ("hermes.radio", &P_HERMES_RADIO), + ("hey.com", &P_HEY_COM), + ("i.ua", &P_I_UA), + ("i3.net", &P_I3_NET), + ("icloud", &P_ICLOUD), + ("infomaniak.com", &P_INFOMANIAK_COM), + ("kolst.com", &P_KOLST_COM), + ("kontent.com", &P_KONTENT_COM), + ("mail.com", &P_MAIL_COM), + ("mail.de", &P_MAIL_DE), + ("mail.ru", &P_MAIL_RU), + ("mail2tor", &P_MAIL2TOR), + ("mailbox.org", &P_MAILBOX_ORG), + ("mailo.com", &P_MAILO_COM), + ("mehl.cloud", &P_MEHL_CLOUD), + ("mehl.store", &P_MEHL_STORE), + ("migadu", &P_MIGADU), + ("nauta.cu", &P_NAUTA_CU), + ("naver", &P_NAVER), + ("nine.testrun.org", &P_NINE_TESTRUN_ORG), + ("nubo.coop", &P_NUBO_COOP), + ("outlook.com", &P_OUTLOOK_COM), + ("ouvaton.coop", &P_OUVATON_COOP), + ("posteo", &P_POSTEO), + ("protonmail", &P_PROTONMAIL), + ("purelymail.com", &P_PURELYMAIL_COM), + ("qq", &P_QQ), + ("rambler.ru", &P_RAMBLER_RU), + ("riseup.net", &P_RISEUP_NET), + ("rogers.com", &P_ROGERS_COM), + ("sonic", &P_SONIC), + ("stinpriza.net", &P_STINPRIZA_NET), + ("systemausfall.org", &P_SYSTEMAUSFALL_ORG), + ("systemli.org", &P_SYSTEMLI_ORG), + ("t-online", &P_T_ONLINE), + ("testrun", &P_TESTRUN), + ("tiscali.it", &P_TISCALI_IT), + ("tutanota", &P_TUTANOTA), + ("ukr.net", &P_UKR_NET), + ("undernet.uy", &P_UNDERNET_UY), + ("vfemail", &P_VFEMAIL), + ("vivaldi", &P_VIVALDI), + ("vk.com", &P_VK_COM), + ("vodafone.de", &P_VODAFONE_DE), + ("web.de", &P_WEB_DE), + ("wkpb.de", &P_WKPB_DE), + ("yahoo", &P_YAHOO), + ("yandex.ru", &P_YANDEX_RU), + ("yggmail", &P_YGGMAIL), + ("ziggo.nl", &P_ZIGGO_NL), + ("zoho", &P_ZOHO), + ]) + }); + +pub static _PROVIDER_UPDATED: LazyLock = + LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap()); diff --git a/src/qr.rs b/src/qr.rs index f9ef653e95..edfd62c120 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -2,15 +2,15 @@ mod dclogin_scheme; use std::collections::BTreeMap; +use std::sync::LazyLock; use anyhow::{anyhow, bail, ensure, Context as _, Result}; pub use dclogin_scheme::LoginOptions; use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress}; -use once_cell::sync::Lazy; use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; -use self::dclogin_scheme::configure_from_login_qr; +pub(crate) use self::dclogin_scheme::configure_from_login_qr; use crate::chat::ChatIdBlocked; use crate::config::Config; use crate::constants::Blocked; @@ -40,7 +40,11 @@ const HTTPS_SCHEME: &str = "https://"; const SHADOWSOCKS_SCHEME: &str = "ss://"; /// Backup transfer based on iroh-net. -pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:"; +pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP"; + +/// Version written to Backups and Backup-QR-Codes. +/// Imports will fail when they have a larger version. +pub(crate) const DCBACKUP_VERSION: i32 = 2; /// Scanned QR code. #[derive(Debug, Clone, PartialEq, Eq)] @@ -118,6 +122,9 @@ pub enum Qr { auth_token: String, }, + /// The QR code is a backup, but it is too new. The user has to update its Delta Chat. + BackupTooNew {}, + /// Ask the user if they want to use the given service for video chats. WebrtcInstance { /// Server domain name. @@ -261,6 +268,13 @@ pub enum Qr { }, } +// hack around the changed JSON accidentally used by an iroh upgrade, see #6518 for more details and for code snippet. +// this hack is mainly needed to give ppl time to upgrade and can be removed after some months (added 2025-02) +fn fix_add_second_device_qr(qr: &str) -> String { + qr.replacen(r#","info":{"relay_url":"#, r#","relay_url":"#, 1) + .replacen(r#""]}}"#, r#""]}"#, 1) +} + fn starts_with_ignore_case(string: &str, pattern: &str) -> bool { string.to_lowercase().starts_with(&pattern.to_lowercase()) } @@ -289,8 +303,9 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { decode_tg_socks_proxy(context, qr)? } else if qr.starts_with(SHADOWSOCKS_SCHEME) { decode_shadowsocks_proxy(qr)? - } else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) { - decode_backup2(qr)? + } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME_PREFIX) { + let qr_fixed = fix_add_second_device_qr(qr); + decode_backup2(&qr_fixed)? } else if qr.starts_with(MAILTO_SCHEME) { decode_mailto(context, qr).await? } else if qr.starts_with(SMTP_SCHEME) { @@ -359,7 +374,9 @@ pub fn format_backup(qr: &Qr) -> Result { ref auth_token, } => { let node_addr = serde_json::to_string(node_addr)?; - Ok(format!("{DCBACKUP2_SCHEME}{auth_token}&{node_addr}")) + Ok(format!( + "{DCBACKUP_SCHEME_PREFIX}{DCBACKUP_VERSION}:{auth_token}&{node_addr}" + )) } _ => Err(anyhow!("Not a backup QR code")), } @@ -635,11 +652,19 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result { }) } -/// Decodes a [`DCBACKUP2_SCHEME`] QR code. +/// Decodes a `DCBACKUP` QR code. fn decode_backup2(qr: &str) -> Result { - let payload = qr - .strip_prefix(DCBACKUP2_SCHEME) - .ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?; + let version_and_payload = qr + .strip_prefix(DCBACKUP_SCHEME_PREFIX) + .ok_or_else(|| anyhow!("Invalid DCBACKUP scheme"))?; + let (version, payload) = version_and_payload + .split_once(':') + .context("DCBACKUP scheme separator missing")?; + let version: i32 = version.parse().context("Not a valid number")?; + if version > DCBACKUP_VERSION { + return Ok(Qr::BackupTooNew {}); + } + let (auth_token, node_addr) = payload .split_once('&') .context("Backup QR code has no separator")?; @@ -670,7 +695,7 @@ struct CreateAccountErrorResponse { /// take a qr of the type DC_QR_ACCOUNT, parse it's parameters, /// download additional information from the contained url and set the parameters. /// on success, a configure::configure() should be able to log in to the account -async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { +pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = qr .get(DCACCOUNT_SCHEME.len()..) .context("Invalid DCACCOUNT scheme")?; @@ -890,10 +915,10 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result { Qr::from_address(context, name, &addr, None).await } -static VCARD_NAME_RE: Lazy = - Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap()); -static VCARD_EMAIL_RE: Lazy = - Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap()); +static VCARD_NAME_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap()); +static VCARD_EMAIL_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap()); /// Extract address for the vcard scheme. /// @@ -947,911 +972,4 @@ fn normalize_address(addr: &str) -> Result { } #[cfg(test)] -mod tests { - use super::*; - use crate::aheader::EncryptPreference; - use crate::chat::{create_group_chat, ProtectionStatus}; - use crate::config::Config; - use crate::key::DcKey; - use crate::securejoin::get_securejoin_qr; - use crate::test_utils::{alice_keypair, TestContext}; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_http() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "http://www.hello.com:80".to_string(), - host: "www.hello.com".to_string(), - port: 80 - } - ); - - // If it has no explicit port, then it is not a proxy. - let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?; - assert_eq!( - qr, - Qr::Url { - url: "http://www.hello.com".to_string(), - } - ); - - // If it has a path, then it is not a proxy. - let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?; - assert_eq!( - qr, - Qr::Url { - url: "http://www.hello.com/".to_string(), - } - ); - let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?; - assert_eq!( - qr, - Qr::Url { - url: "http://www.hello.com/hello".to_string(), - } - ); - - // Test that QR code whitespace is stripped. - // Users can copy-paste QR code contents and "scan" - // from the clipboard. - let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?; - assert_eq!( - qr, - Qr::Url { - url: "http://www.hello.com/hello".to_string(), - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_https() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "https://www.hello.com:443".to_string(), - host: "www.hello.com".to_string(), - port: 443 - } - ); - - // If it has no explicit port, then it is not a proxy. - let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?; - assert_eq!( - qr, - Qr::Url { - url: "https://www.hello.com".to_string(), - } - ); - - // If it has a path, then it is not a proxy. - let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?; - assert_eq!( - qr, - Qr::Url { - url: "https://www.hello.com/".to_string(), - } - ); - let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?; - assert_eq!( - qr, - Qr::Url { - url: "https://www.hello.com/hello".to_string(), - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_text() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr(&ctx.ctx, "I am so cool").await?; - assert_eq!( - qr, - Qr::Text { - text: "I am so cool".to_string() - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_vcard() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD" - ).await?; - - if let Qr::Addr { contact_id, draft } = qr { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "stress@test.local"); - assert_eq!(contact.get_name(), "First Last"); - assert_eq!(contact.get_authname(), ""); - assert_eq!(contact.get_display_name(), "First Last"); - assert!(draft.is_none()); - } else { - bail!("Wrong QR code type"); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_matmsg() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;", - ) - .await?; - - if let Qr::Addr { contact_id, draft } = qr { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "stress@test.local"); - assert!(draft.is_none()); - } else { - bail!("Wrong QR code type"); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_mailto() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "mailto:stress@test.local?subject=hello&body=beautiful+world", - ) - .await?; - if let Qr::Addr { contact_id, draft } = qr { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "stress@test.local"); - assert_eq!(draft.unwrap(), "hello\nbeautiful world"); - } else { - bail!("Wrong QR code type"); - } - - let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?; - if let Qr::Addr { contact_id, draft } = res { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "no-questionmark@example.org"); - assert!(draft.is_none()); - } else { - bail!("Wrong QR code type"); - } - - let res = check_qr(&ctx.ctx, "mailto:no-addr").await; - assert!(res.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_smtp() -> Result<()> { - let ctx = TestContext::new().await; - - if let Qr::Addr { contact_id, draft } = - check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await? - { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "stress@test.local"); - assert!(draft.is_none()); - } else { - bail!("Wrong QR code type"); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_ideltachat_link() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" - ).await?; - assert!(matches!(qr, Qr::AskVerifyGroup { .. })); - - let qr = check_qr( - &ctx.ctx, - "https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" - ).await?; - assert!(matches!(qr, Qr::AskVerifyGroup { .. })); - - Ok(()) - } - - // macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too. - // see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" - ).await?; - - assert!(matches!(qr, Qr::AskVerifyGroup { .. })); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_openpgp_group() -> Result<()> { - let ctx = TestContext::new().await; - 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=0V7LzL9cxRL" - ).await?; - if let Qr::AskVerifyGroup { - contact_id, - grpname, - .. - } = qr - { - assert_ne!(contact_id, ContactId::UNDEFINED); - assert_eq!(grpname, "test ? test !"); - } else { - bail!("Wrong QR code type"); - } - - // Test it again with lowercased "openpgp4fpr:" uri scheme - let ctx = TestContext::new().await; - 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=0V7LzL9cxRL" - ).await?; - if let Qr::AskVerifyGroup { - contact_id, - grpname, - .. - } = qr - { - assert_ne!(contact_id, ContactId::UNDEFINED); - assert_eq!(grpname, "test ? test !"); - - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "cli@deltachat.de"); - } else { - bail!("Wrong QR code type"); - } - - 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; - - let qr = check_qr( - &ctx.ctx, - "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" - ).await?; - - if let Qr::AskVerifyContact { contact_id, .. } = qr { - assert_ne!(contact_id, ContactId::UNDEFINED); - } else { - bail!("Wrong QR code type"); - } - - // Test it again with lowercased "openpgp4fpr:" uri scheme - let qr = check_qr( - &ctx.ctx, - "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" - ).await?; - - if let Qr::AskVerifyContact { contact_id, .. } = qr { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "cli@deltachat.de"); - assert_eq!(contact.get_authname(), "Jörn P. P."); - assert_eq!(contact.get_name(), ""); - } else { - bail!("Wrong QR code type"); - } - - // Regression test - let ctx = TestContext::new().await; - let qr = check_qr( - &ctx.ctx, - "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" - ).await?; - - if let Qr::AskVerifyContact { contact_id, .. } = qr { - let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; - assert_eq!(contact.get_addr(), "cli@deltachat.de"); - assert_eq!(contact.get_authname(), ""); - assert_eq!(contact.get_name(), ""); - } else { - bail!("Wrong QR code type"); - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_openpgp_fingerprint() -> Result<()> { - let ctx = TestContext::new().await; - - let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org") - .await - .context("failed to create contact")?; - let pub_key = alice_keypair().public; - let peerstate = Peerstate { - addr: "alice@example.org".to_string(), - last_seen: 1, - last_seen_autocrypt: 1, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(pub_key.clone()), - public_key_fingerprint: Some(pub_key.dc_fingerprint()), - gossip_key: None, - gossip_timestamp: 0, - gossip_key_fingerprint: None, - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - assert!( - peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), - "failed to save peerstate" - ); - - let qr = check_qr( - &ctx.ctx, - "OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org", - ) - .await?; - if let Qr::FprMismatch { contact_id, .. } = qr { - assert_eq!(contact_id, Some(alice_contact_id)); - } else { - bail!("Wrong QR code type"); - } - - let qr = check_qr( - &ctx.ctx, - &format!( - "OPENPGP4FPR:{}#a=alice@example.org", - pub_key.dc_fingerprint() - ), - ) - .await?; - if let Qr::FprOk { contact_id, .. } = qr { - assert_eq!(contact_id, alice_contact_id); - } else { - bail!("Wrong QR code type"); - } - - assert_eq!( - check_qr( - &ctx.ctx, - "OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org", - ) - .await?, - Qr::FprMismatch { contact_id: None } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_openpgp_without_addr() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "OPENPGP4FPR:1234567890123456789012345678901234567890", - ) - .await?; - assert_eq!( - qr, - Qr::FprWithoutAddr { - fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string() - } - ); - - // Test it again with lowercased "openpgp4fpr:" uri scheme - - let qr = check_qr( - &ctx.ctx, - "openpgp4fpr:1234567890123456789012345678901234567890", - ) - .await?; - assert_eq!( - qr, - Qr::FprWithoutAddr { - fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string() - } - ); - - let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await; - assert!(res.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_withdraw_verifycontact() -> Result<()> { - let alice = TestContext::new_alice().await; - let qr = get_securejoin_qr(&alice, None).await?; - - // scanning own verify-contact code offers withdrawing - assert!(matches!( - check_qr(&alice, &qr).await?, - Qr::WithdrawVerifyContact { .. } - )); - set_config_from_qr(&alice, &qr).await?; - - // scanning withdrawn verify-contact code offers reviving - assert!(matches!( - check_qr(&alice, &qr).await?, - Qr::ReviveVerifyContact { .. } - )); - set_config_from_qr(&alice, &qr).await?; - assert!(matches!( - check_qr(&alice, &qr).await?, - Qr::WithdrawVerifyContact { .. } - )); - - // someone else always scans as ask-verify-contact - let bob = TestContext::new_bob().await; - assert!(matches!( - check_qr(&bob, &qr).await?, - Qr::AskVerifyContact { .. } - )); - assert!(set_config_from_qr(&bob, &qr).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_withdraw_verifygroup() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - let qr = get_securejoin_qr(&alice, Some(chat_id)).await?; - - // scanning own verify-group code offers withdrawing - if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? { - assert_eq!(grpname, "foo"); - } else { - bail!("Wrong QR type, expected WithdrawVerifyGroup"); - } - set_config_from_qr(&alice, &qr).await?; - - // scanning withdrawn verify-group code offers reviving - if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? { - assert_eq!(grpname, "foo"); - } else { - bail!("Wrong QR type, expected ReviveVerifyGroup"); - } - - // someone else always scans as ask-verify-group - let bob = TestContext::new_bob().await; - if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? { - assert_eq!(grpname, "foo"); - } else { - bail!("Wrong QR type, expected AskVerifyGroup"); - } - assert!(set_config_from_qr(&bob, &qr).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_and_apply_dclogin() -> Result<()> { - let ctx = TestContext::new().await; - - let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?; - if let Qr::Login { address, options } = result { - assert_eq!(address, "usename+extension@host".to_owned()); - - if let LoginOptions::V1 { mail_pw, .. } = options { - assert_eq!(mail_pw, "1234".to_owned()); - } else { - bail!("wrong type") - } - } else { - bail!("wrong type") - } - - assert!(ctx.ctx.get_config(Config::Addr).await?.is_none()); - assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none()); - - set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?; - assert_eq!( - ctx.ctx.get_config(Config::Addr).await?, - Some("username+extension@host".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::MailPw).await?, - Some("1234".to_owned()) - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> { - let ctx = TestContext::new().await; - set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?; - assert_eq!( - ctx.ctx.get_config(Config::Addr).await?, - Some("username+extension@host".to_owned()) - ); - - // `p=1234` is ignored, because `ipw=password` is set - - assert_eq!( - ctx.ctx.get_config(Config::MailServer).await?, - Some("host.tld".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::MailPort).await?, - Some("4343".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::MailUser).await?, - Some("user".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::MailPw).await?, - Some("password".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::MailSecurity).await?, - Some("1".to_owned()) // ssl - ); - assert_eq!( - ctx.ctx.get_config(Config::ImapCertificateChecks).await?, - Some("1".to_owned()) - ); - - assert_eq!( - ctx.ctx.get_config(Config::SendPw).await?, - Some("4321".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::SendServer).await?, - Some("send.host".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::SendPort).await?, - Some("7273".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::SendUser).await?, - Some("SendUser".to_owned()) - ); - - // `sc` option is actually ignored and `ic` is used instead - // because `smtp_certificate_checks` is deprecated. - assert_eq!( - ctx.ctx.get_config(Config::SmtpCertificateChecks).await?, - Some("1".to_owned()) - ); - assert_eq!( - ctx.ctx.get_config(Config::SendSecurity).await?, - Some("3".to_owned()) // plain - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_account() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ) - .await?; - assert_eq!( - qr, - Qr::Account { - domain: "example.org".to_string() - } - ); - - // Test it again with lowercased "dcaccount:" uri scheme - let qr = check_qr( - &ctx.ctx, - "dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ) - .await?; - assert_eq!( - qr, - Qr::Account { - domain: "example.org".to_string() - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_webrtc_instance() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?; - assert_eq!( - qr, - Qr::WebrtcInstance { - domain: "basicurl.com".to_string(), - instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string() - } - ); - - // Test it again with mixcased "dcWebRTC:" uri scheme - let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?; - assert_eq!( - qr, - Qr::WebrtcInstance { - domain: "example.org".to_string(), - instance_pattern: "https://example.org/".to_string() - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_tg_socks_proxy() -> Result<()> { - let t = TestContext::new().await; - - let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "socks5://84.53.239.95:4145".to_string(), - host: "84.53.239.95".to_string(), - port: 4145, - } - ); - - let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "socks5://foo.bar:123".to_string(), - host: "foo.bar".to_string(), - port: 123, - } - ); - - let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "socks5://foo.baz:1080".to_string(), - host: "foo.baz".to_string(), - port: 1080, - } - ); - - let qr = check_qr( - &t, - "https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24", - ) - .await?; - assert_eq!( - qr, - Qr::Proxy { - url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(), - host: "foo.baz".to_string(), - port: 12345, - } - ); - - // wrong domain results in Qr:Url instead of Qr::Socks5Proxy - let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?; - assert_eq!( - qr, - Qr::Url { - url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string() - } - ); - - let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await; - assert!(qr.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_account_bad_scheme() { - let ctx = TestContext::new().await; - let res = check_qr( - &ctx.ctx, - "DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ) - .await; - assert!(res.is_err()); - - // Test it again with lowercased "dcaccount:" uri scheme - let res = check_qr( - &ctx.ctx, - "dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", - ) - .await; - assert!(res.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_webrtc_instance_config_from_qr() -> Result<()> { - let ctx = TestContext::new().await; - - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); - - let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await; - assert!(res.is_err()); - assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); - - let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await; - assert!(res.is_ok()); - assert_eq!( - ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(), - "https://example.org/" - ); - - let res = - set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await; - assert!(res.is_ok()); - assert_eq!( - ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(), - "basicwebrtc:https://foo.bar/?$ROOM&test" - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_proxy_config_from_qr() -> Result<()> { - let t = TestContext::new().await; - - assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false); - - let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; - assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some("socks5://foo:666".to_string()) - ); - - // Test URL without port. - // - // Also check that whitespace is trimmed. - let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await; - assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string()) - ); - - // make sure, user&password are set when specified in the URL - // Password is an URL-encoded "x&%$X". - let res = - set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await; - assert!(res.is_ok()); - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some( - "socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666" - .to_string() - ) - ); - - // Scanning existing proxy brings it to the top in the list. - let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; - assert!(res.is_ok()); - assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some( - "socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080" - .to_string() - ) - ); - - set_config_from_qr( - &t, - "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", - ) - .await?; - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some( - "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080" - .to_string() - ) - ); - - // SOCKS5 config does not have port 1080 explicitly specified, - // but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry. - set_config_from_qr(&t, "socks5://1.2.3.4").await?; - assert_eq!( - t.get_config(Config::ProxyUrl).await?, - Some( - "socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080" - .to_string() - ) - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_shadowsocks() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr( - &ctx.ctx, - "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", - ) - .await?; - assert_eq!( - qr, - Qr::Proxy { - url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(), - host: "192.168.100.1".to_string(), - port: 8888, - } - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decode_socks5() -> Result<()> { - let ctx = TestContext::new().await; - - let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?; - assert_eq!( - qr, - Qr::Proxy { - url: "socks5://127.0.0.1:9050".to_string(), - host: "127.0.0.1".to_string(), - port: 9050, - } - ); - - Ok(()) - } -} +mod qr_tests; diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs new file mode 100644 index 0000000000..fa507b1bdc --- /dev/null +++ b/src/qr/qr_tests.rs @@ -0,0 +1,927 @@ +use super::*; +use crate::aheader::EncryptPreference; +use crate::chat::{create_group_chat, ProtectionStatus}; +use crate::config::Config; +use crate::key::DcKey; +use crate::securejoin::get_securejoin_qr; +use crate::test_utils::{alice_keypair, TestContext}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_http() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "http://www.hello.com:80".to_string(), + host: "www.hello.com".to_string(), + port: 80 + } + ); + + // If it has no explicit port, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com".to_string(), + } + ); + + // If it has a path, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com/".to_string(), + } + ); + let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com/hello".to_string(), + } + ); + + // Test that QR code whitespace is stripped. + // Users can copy-paste QR code contents and "scan" + // from the clipboard. + let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?; + assert_eq!( + qr, + Qr::Url { + url: "http://www.hello.com/hello".to_string(), + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_https() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "https://www.hello.com:443".to_string(), + host: "www.hello.com".to_string(), + port: 443 + } + ); + + // If it has no explicit port, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://www.hello.com".to_string(), + } + ); + + // If it has a path, then it is not a proxy. + let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://www.hello.com/".to_string(), + } + ); + let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://www.hello.com/hello".to_string(), + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_text() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "I am so cool").await?; + assert_eq!( + qr, + Qr::Text { + text: "I am so cool".to_string() + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_vcard() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD", + ) + .await?; + + if let Qr::Addr { contact_id, draft } = qr { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "stress@test.local"); + assert_eq!(contact.get_name(), "First Last"); + assert_eq!(contact.get_authname(), ""); + assert_eq!(contact.get_display_name(), "First Last"); + assert!(draft.is_none()); + } else { + bail!("Wrong QR code type"); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_matmsg() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;", + ) + .await?; + + if let Qr::Addr { contact_id, draft } = qr { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "stress@test.local"); + assert!(draft.is_none()); + } else { + bail!("Wrong QR code type"); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_mailto() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "mailto:stress@test.local?subject=hello&body=beautiful+world", + ) + .await?; + if let Qr::Addr { contact_id, draft } = qr { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "stress@test.local"); + assert_eq!(draft.unwrap(), "hello\nbeautiful world"); + } else { + bail!("Wrong QR code type"); + } + + let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?; + if let Qr::Addr { contact_id, draft } = res { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "no-questionmark@example.org"); + assert!(draft.is_none()); + } else { + bail!("Wrong QR code type"); + } + + let res = check_qr(&ctx.ctx, "mailto:no-addr").await; + assert!(res.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_smtp() -> Result<()> { + let ctx = TestContext::new().await; + + if let Qr::Addr { contact_id, draft } = + check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await? + { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "stress@test.local"); + assert!(draft.is_none()); + } else { + bail!("Wrong QR code type"); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_ideltachat_link() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" + ).await?; + assert!(matches!(qr, Qr::AskVerifyGroup { .. })); + + let qr = check_qr( + &ctx.ctx, + "https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" + ).await?; + assert!(matches!(qr, Qr::AskVerifyGroup { .. })); + + Ok(()) +} + +// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too. +// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" + ).await?; + + assert!(matches!(qr, Qr::AskVerifyGroup { .. })); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_openpgp_group() -> Result<()> { + let ctx = TestContext::new().await; + 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=0V7LzL9cxRL" + ).await?; + if let Qr::AskVerifyGroup { + contact_id, + grpname, + .. + } = qr + { + assert_ne!(contact_id, ContactId::UNDEFINED); + assert_eq!(grpname, "test ? test !"); + } else { + bail!("Wrong QR code type"); + } + + // Test it again with lowercased "openpgp4fpr:" uri scheme + let ctx = TestContext::new().await; + 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=0V7LzL9cxRL" + ).await?; + if let Qr::AskVerifyGroup { + contact_id, + grpname, + .. + } = qr + { + assert_ne!(contact_id, ContactId::UNDEFINED); + assert_eq!(grpname, "test ? test !"); + + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "cli@deltachat.de"); + } else { + bail!("Wrong QR code type"); + } + + 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; + + let qr = check_qr( + &ctx.ctx, + "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" + ).await?; + + if let Qr::AskVerifyContact { contact_id, .. } = qr { + assert_ne!(contact_id, ContactId::UNDEFINED); + } else { + bail!("Wrong QR code type"); + } + + // Test it again with lowercased "openpgp4fpr:" uri scheme + let qr = check_qr( + &ctx.ctx, + "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" + ).await?; + + if let Qr::AskVerifyContact { contact_id, .. } = qr { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "cli@deltachat.de"); + assert_eq!(contact.get_authname(), "Jörn P. P."); + assert_eq!(contact.get_name(), ""); + } else { + bail!("Wrong QR code type"); + } + + // Regression test + let ctx = TestContext::new().await; + let qr = check_qr( + &ctx.ctx, + "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" + ).await?; + + if let Qr::AskVerifyContact { contact_id, .. } = qr { + let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?; + assert_eq!(contact.get_addr(), "cli@deltachat.de"); + assert_eq!(contact.get_authname(), ""); + assert_eq!(contact.get_name(), ""); + } else { + bail!("Wrong QR code type"); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_openpgp_fingerprint() -> Result<()> { + let ctx = TestContext::new().await; + + let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org") + .await + .context("failed to create contact")?; + let pub_key = alice_keypair().public; + let peerstate = Peerstate { + addr: "alice@example.org".to_string(), + last_seen: 1, + last_seen_autocrypt: 1, + prefer_encrypt: EncryptPreference::Mutual, + public_key: Some(pub_key.clone()), + public_key_fingerprint: Some(pub_key.dc_fingerprint()), + gossip_key: None, + gossip_timestamp: 0, + gossip_key_fingerprint: None, + verified_key: None, + verified_key_fingerprint: None, + verifier: None, + secondary_verified_key: None, + secondary_verified_key_fingerprint: None, + secondary_verifier: None, + backward_verified_key_id: None, + fingerprint_changed: false, + }; + assert!( + peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), + "failed to save peerstate" + ); + + let qr = check_qr( + &ctx.ctx, + "OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org", + ) + .await?; + if let Qr::FprMismatch { contact_id, .. } = qr { + assert_eq!(contact_id, Some(alice_contact_id)); + } else { + bail!("Wrong QR code type"); + } + + let qr = check_qr( + &ctx.ctx, + &format!( + "OPENPGP4FPR:{}#a=alice@example.org", + pub_key.dc_fingerprint() + ), + ) + .await?; + if let Qr::FprOk { contact_id, .. } = qr { + assert_eq!(contact_id, alice_contact_id); + } else { + bail!("Wrong QR code type"); + } + + assert_eq!( + check_qr( + &ctx.ctx, + "OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org", + ) + .await?, + Qr::FprMismatch { contact_id: None } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_openpgp_without_addr() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "OPENPGP4FPR:1234567890123456789012345678901234567890", + ) + .await?; + assert_eq!( + qr, + Qr::FprWithoutAddr { + fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string() + } + ); + + // Test it again with lowercased "openpgp4fpr:" uri scheme + + let qr = check_qr( + &ctx.ctx, + "openpgp4fpr:1234567890123456789012345678901234567890", + ) + .await?; + assert_eq!( + qr, + Qr::FprWithoutAddr { + fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string() + } + ); + + let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await; + assert!(res.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_withdraw_verifycontact() -> Result<()> { + let alice = TestContext::new_alice().await; + let qr = get_securejoin_qr(&alice, None).await?; + + // scanning own verify-contact code offers withdrawing + assert!(matches!( + check_qr(&alice, &qr).await?, + Qr::WithdrawVerifyContact { .. } + )); + set_config_from_qr(&alice, &qr).await?; + + // scanning withdrawn verify-contact code offers reviving + assert!(matches!( + check_qr(&alice, &qr).await?, + Qr::ReviveVerifyContact { .. } + )); + set_config_from_qr(&alice, &qr).await?; + assert!(matches!( + check_qr(&alice, &qr).await?, + Qr::WithdrawVerifyContact { .. } + )); + + // someone else always scans as ask-verify-contact + let bob = TestContext::new_bob().await; + assert!(matches!( + check_qr(&bob, &qr).await?, + Qr::AskVerifyContact { .. } + )); + assert!(set_config_from_qr(&bob, &qr).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_withdraw_verifygroup() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let qr = get_securejoin_qr(&alice, Some(chat_id)).await?; + + // scanning own verify-group code offers withdrawing + if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? { + assert_eq!(grpname, "foo"); + } else { + bail!("Wrong QR type, expected WithdrawVerifyGroup"); + } + set_config_from_qr(&alice, &qr).await?; + + // scanning withdrawn verify-group code offers reviving + if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? { + assert_eq!(grpname, "foo"); + } else { + bail!("Wrong QR type, expected ReviveVerifyGroup"); + } + + // someone else always scans as ask-verify-group + let bob = TestContext::new_bob().await; + if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? { + assert_eq!(grpname, "foo"); + } else { + bail!("Wrong QR type, expected AskVerifyGroup"); + } + assert!(set_config_from_qr(&bob, &qr).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_and_apply_dclogin() -> Result<()> { + let ctx = TestContext::new().await; + + let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?; + if let Qr::Login { address, options } = result { + assert_eq!(address, "usename+extension@host".to_owned()); + + if let LoginOptions::V1 { mail_pw, .. } = options { + assert_eq!(mail_pw, "1234".to_owned()); + } else { + bail!("wrong type") + } + } else { + bail!("wrong type") + } + + assert!(ctx.ctx.get_config(Config::Addr).await?.is_none()); + assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none()); + + set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?; + assert_eq!( + ctx.ctx.get_config(Config::Addr).await?, + Some("username+extension@host".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::MailPw).await?, + Some("1234".to_owned()) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> { + let ctx = TestContext::new().await; + set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?; + assert_eq!( + ctx.ctx.get_config(Config::Addr).await?, + Some("username+extension@host".to_owned()) + ); + + // `p=1234` is ignored, because `ipw=password` is set + + assert_eq!( + ctx.ctx.get_config(Config::MailServer).await?, + Some("host.tld".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::MailPort).await?, + Some("4343".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::MailUser).await?, + Some("user".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::MailPw).await?, + Some("password".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::MailSecurity).await?, + Some("1".to_owned()) // ssl + ); + assert_eq!( + ctx.ctx.get_config(Config::ImapCertificateChecks).await?, + Some("1".to_owned()) + ); + + assert_eq!( + ctx.ctx.get_config(Config::SendPw).await?, + Some("4321".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::SendServer).await?, + Some("send.host".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::SendPort).await?, + Some("7273".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::SendUser).await?, + Some("SendUser".to_owned()) + ); + + // `sc` option is actually ignored and `ic` is used instead + // because `smtp_certificate_checks` is deprecated. + assert_eq!( + ctx.ctx.get_config(Config::SmtpCertificateChecks).await?, + Some("1".to_owned()) + ); + assert_eq!( + ctx.ctx.get_config(Config::SendSecurity).await?, + Some("3".to_owned()) // plain + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_account() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", + ) + .await?; + assert_eq!( + qr, + Qr::Account { + domain: "example.org".to_string() + } + ); + + // Test it again with lowercased "dcaccount:" uri scheme + let qr = check_qr( + &ctx.ctx, + "dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", + ) + .await?; + assert_eq!( + qr, + Qr::Account { + domain: "example.org".to_string() + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_webrtc_instance() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?; + assert_eq!( + qr, + Qr::WebrtcInstance { + domain: "basicurl.com".to_string(), + instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string() + } + ); + + // Test it again with mixcased "dcWebRTC:" uri scheme + let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?; + assert_eq!( + qr, + Qr::WebrtcInstance { + domain: "example.org".to_string(), + instance_pattern: "https://example.org/".to_string() + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_tg_socks_proxy() -> Result<()> { + let t = TestContext::new().await; + + let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://84.53.239.95:4145".to_string(), + host: "84.53.239.95".to_string(), + port: 4145, + } + ); + + let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://foo.bar:123".to_string(), + host: "foo.bar".to_string(), + port: 123, + } + ); + + let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://foo.baz:1080".to_string(), + host: "foo.baz".to_string(), + port: 1080, + } + ); + + let qr = check_qr( + &t, + "https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24", + ) + .await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(), + host: "foo.baz".to_string(), + port: 12345, + } + ); + + // wrong domain results in Qr:Url instead of Qr::Socks5Proxy + let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?; + assert_eq!( + qr, + Qr::Url { + url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string() + } + ); + + let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await; + assert!(qr.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_account_bad_scheme() { + let ctx = TestContext::new().await; + let res = check_qr( + &ctx.ctx, + "DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", + ) + .await; + assert!(res.is_err()); + + // Test it again with lowercased "dcaccount:" uri scheme + let res = check_qr( + &ctx.ctx, + "dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", + ) + .await; + assert!(res.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_webrtc_instance_config_from_qr() -> Result<()> { + let ctx = TestContext::new().await; + + assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); + + let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await; + assert!(res.is_err()); + assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none()); + + let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await; + assert!(res.is_ok()); + assert_eq!( + ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(), + "https://example.org/" + ); + + let res = + set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await; + assert!(res.is_ok()); + assert_eq!( + ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(), + "basicwebrtc:https://foo.bar/?$ROOM&test" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_proxy_config_from_qr() -> Result<()> { + let t = TestContext::new().await; + + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false); + + let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; + assert!(res.is_ok()); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some("socks5://foo:666".to_string()) + ); + + // Test URL without port. + // + // Also check that whitespace is trimmed. + let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await; + assert!(res.is_ok()); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string()) + ); + + // make sure, user&password are set when specified in the URL + // Password is an URL-encoded "x&%$X". + let res = + set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await; + assert!(res.is_ok()); + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666".to_string() + ) + ); + + // Scanning existing proxy brings it to the top in the list. + let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await; + assert!(res.is_ok()); + assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true); + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080".to_string() + ) + ); + + set_config_from_qr( + &t, + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", + ) + .await?; + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080" + .to_string() + ) + ); + + // SOCKS5 config does not have port 1080 explicitly specified, + // but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry. + set_config_from_qr(&t, "socks5://1.2.3.4").await?; + assert_eq!( + t.get_config(Config::ProxyUrl).await?, + Some( + "socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080" + .to_string() + ) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_shadowsocks() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr( + &ctx.ctx, + "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1", + ) + .await?; + assert_eq!( + qr, + Qr::Proxy { + url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(), + host: "192.168.100.1".to_string(), + port: 8888, + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_socks5() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?; + assert_eq!( + qr, + Qr::Proxy { + url: "socks5://127.0.0.1:9050".to_string(), + host: "127.0.0.1".to_string(), + port: 9050, + } + ); + + Ok(()) +} + +/// Ensure that `DCBACKUP2` QR code does not fail to deserialize +/// because iroh changes the format of `NodeAddr` +/// as happened between iroh 0.29 and iroh 0.30 before. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_decode_backup() -> Result<()> { + let ctx = TestContext::new().await; + + let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?; + assert!(matches!(qr, Qr::Backup2 { .. })); + + let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?; + assert!(matches!(qr, Qr::Backup2 { .. })); + + let qr = check_qr(&ctx, r#"DCBACKUP9:from-the-future"#).await?; + assert!(matches!(qr, Qr::BackupTooNew { .. })); + + let qr = check_qr(&ctx, r#"DCBACKUP99:far-from-the-future"#).await?; + assert!(matches!(qr, Qr::BackupTooNew { .. })); + + Ok(()) +} diff --git a/src/reaction.rs b/src/reaction.rs index a54d0eb4d9..985bdc15d2 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -398,7 +398,7 @@ mod tests { use deltachat_contact_tools::ContactAddress; use super::*; - use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg}; + use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::contact::{Contact, Origin}; @@ -623,7 +623,9 @@ Here's my footer -- bob@example.net" .get_matching_opt(t, |evt| { matches!( evt, - EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. } + EventType::IncomingReaction { .. } + | EventType::IncomingMsg { .. } + | EventType::MsgsChanged { .. } ) }) .await; @@ -667,7 +669,8 @@ Here's my footer -- bob@example.net" assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2); let bob_reaction_msg = bob.pop_sent_msg().await; - alice.recv_msg_trash(&bob_reaction_msg).await; + let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await; + assert_eq!(alice_reaction_msg.state, MessageState::InFresh); assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2); let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?; @@ -691,6 +694,20 @@ Here's my footer -- bob@example.net" .await?; expect_no_unwanted_events(&alice).await; + marknoticed_chat(&alice, chat_alice.id).await?; + assert_eq!( + alice_reaction_msg.id.get_state(&alice).await?, + MessageState::InSeen + ); + // Reactions don't request MDNs. + assert_eq!( + alice + .sql + .count("SELECT COUNT(*) FROM smtp_mdns", ()) + .await?, + 0 + ); + // Alice reacts to own message. send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀") .await @@ -714,8 +731,9 @@ Here's my footer -- bob@example.net" #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_reaction_summary() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; alice.set_config(Config::Displayname, Some("ALICE")).await?; bob.set_config(Config::Displayname, Some("BOB")).await?; let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await; @@ -730,7 +748,7 @@ Here's my footer -- bob@example.net" bob_msg1.chat_id.accept(&bob).await?; send_reaction(&bob, bob_msg1.id, "👍").await?; let bob_send_reaction = bob.pop_sent_msg().await; - alice.recv_msg_trash(&bob_send_reaction).await; + alice.recv_msg_hidden(&bob_send_reaction).await; expect_incoming_reactions_event( &alice, alice_chat.id, @@ -882,7 +900,6 @@ Here's my footer -- bob@example.net" msg_header.as_bytes(), false, Some(100000), - false, ) .await? .unwrap(); @@ -899,7 +916,7 @@ Here's my footer -- bob@example.net" let bob_reaction_msg = bob.pop_sent_msg().await; // Alice receives a reaction. - alice.recv_msg_trash(&bob_reaction_msg).await; + alice.recv_msg_hidden(&bob_reaction_msg).await; let reactions = get_msg_reactions(&alice, alice_msg_id).await?; assert_eq!(reactions.to_string(), "👍1"); @@ -913,7 +930,6 @@ Here's my footer -- bob@example.net" msg_full.as_bytes(), false, None, - false, ) .await?; @@ -951,7 +967,7 @@ Here's my footer -- bob@example.net" { send_reaction(&alice2, alice2_msg.id, "👍").await?; let msg = alice2.pop_sent_msg().await; - alice1.recv_msg_trash(&msg).await; + alice1.recv_msg_hidden(&msg).await; } // Check that the status is still the same. @@ -973,7 +989,7 @@ Here's my footer -- bob@example.net" let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await; send_reaction(&alice0, alice0_msg_id, "👀").await?; - alice1.recv_msg_trash(&alice0.pop_sent_msg().await).await; + alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await; expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?; expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 6abc92dd60..d5642db1e5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::iter; +use std::sync::LazyLock; use anyhow::{Context as _, Result}; use data_encoding::BASE32_NOPAD; @@ -9,13 +10,12 @@ use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, use iroh_gossip::proto::TopicId; use mailparse::SingleInfo; use num_traits::FromPrimitive; -use once_cell::sync::Lazy; use regex::Regex; use crate::aheader::EncryptPreference; use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; -use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH}; +use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX}; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc_inner; @@ -24,6 +24,7 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX}; +use crate::key::DcKey; use crate::log::LogExt; use crate::message::{ self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype, @@ -54,6 +55,9 @@ pub struct ReceivedMsg { /// Received message state. pub state: MessageState, + /// Whether the message is hidden. + pub hidden: bool, + /// Message timestamp for sorting. pub sort_timestamp: i64, @@ -95,12 +99,11 @@ pub async fn receive_imf( head.as_bytes(), seen, Some(imf_raw.len().try_into()?), - false, ) .await; } } - receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None, false).await + receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await } /// Emulates reception of a message from "INBOX". @@ -113,7 +116,6 @@ pub(crate) async fn receive_imf_from_inbox( imf_raw: &[u8], seen: bool, is_partial_download: Option, - fetching_existing_messages: bool, ) -> Result> { receive_imf_inner( context, @@ -124,7 +126,6 @@ pub(crate) async fn receive_imf_from_inbox( imf_raw, seen, is_partial_download, - fetching_existing_messages, ) .await } @@ -168,7 +169,6 @@ pub(crate) async fn receive_imf_inner( imf_raw: &[u8], seen: bool, is_partial_download: Option, - fetching_existing_messages: bool, ) -> Result> { if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { info!( @@ -192,6 +192,7 @@ pub(crate) async fn receive_imf_inner( return Ok(Some(ReceivedMsg { chat_id: DC_CHAT_ID_TRASH, state: MessageState::Undefined, + hidden: false, sort_timestamp: 0, msg_ids, needs_delete_job: false, @@ -364,7 +365,7 @@ pub(crate) async fn receive_imf_inner( if mime_parser.get_header(HeaderDef::SecureJoin).is_some() { let res; if mime_parser.incoming { - res = handle_securejoin_handshake(context, &mime_parser, from_id) + res = handle_securejoin_handshake(context, &mut mime_parser, from_id) .await .context("error in Secure-Join message handling")?; @@ -385,6 +386,7 @@ pub(crate) async fn receive_imf_inner( received_msg = Some(ReceivedMsg { chat_id: DC_CHAT_ID_TRASH, state: MessageState::InSeen, + hidden: false, sort_timestamp: mime_parser.timestamp_sent, msg_ids: vec![msg_id], needs_delete_job: res == securejoin::HandshakeMessage::Done, @@ -400,8 +402,11 @@ pub(crate) async fn receive_imf_inner( received_msg = None; } - let verified_encryption = - has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?; + let verified_encryption = has_verified_encryption(&mime_parser, from_id)?; + + if verified_encryption == VerifiedEncryption::Verified { + mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?; + } if verified_encryption == VerifiedEncryption::Verified && mime_parser.get_header(HeaderDef::ChatVerified).is_some() @@ -436,7 +441,6 @@ pub(crate) async fn receive_imf_inner( seen, is_partial_download, replace_msg_id, - fetching_existing_messages, prevent_rename, verified_encryption, ) @@ -449,22 +453,25 @@ pub(crate) async fn receive_imf_inner( } // Update gossiped timestamp for the chat if someone else or our other device sent - // Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves + // Autocrypt-Gossip header to avoid sending Autocrypt-Gossip ourselves // and waste traffic. let chat_id = received_msg.chat_id; - if !chat_id.is_special() - && mime_parser.recipients.iter().all(|recipient| { - recipient.addr == mime_parser.from.addr - || mime_parser.gossiped_keys.contains_key(&recipient.addr) - }) - { - info!( - context, - "Received message contains Autocrypt-Gossip for all members of {chat_id}, updating timestamp." - ); - if chat_id.get_gossiped_timestamp(context).await? < mime_parser.timestamp_sent { - chat_id - .set_gossiped_timestamp(context, mime_parser.timestamp_sent) + if !chat_id.is_special() { + for gossiped_key in mime_parser.gossiped_keys.values() { + context + .sql + .transaction(move |transaction| { + let fingerprint = gossiped_key.dc_fingerprint().hex(); + transaction.execute( + "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) + VALUES (?, ?, ?) + ON CONFLICT (chat_id, fingerprint) + DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)", + (chat_id, &fingerprint, mime_parser.timestamp_sent), + )?; + + Ok(()) + }) .await?; } } @@ -619,7 +626,9 @@ pub(crate) async fn receive_imf_inner( } } - if let Some(replace_chat_id) = replace_chat_id { + if received_msg.hidden { + // No need to emit an event about the changed message + } else if let Some(replace_chat_id) = replace_chat_id { context.emit_msgs_changed_without_msg_id(replace_chat_id); } else if !chat_id.is_trash() { let fresh = received_msg.state == MessageState::InFresh; @@ -708,13 +717,10 @@ async fn add_parts( seen: bool, is_partial_download: Option, mut replace_msg_id: Option, - fetching_existing_messages: bool, prevent_rename: bool, verified_encryption: VerifiedEncryption, ) -> Result { let is_bot = context.get_config_bool(Config::Bot).await?; - // Bots handle existing messages the same way as new ones. - let fetching_existing_messages = fetching_existing_messages && !is_bot; let rfc724_mid_orig = &mime_parser .get_rfc724_mid() .unwrap_or(rfc724_mid.to_string()); @@ -723,7 +729,7 @@ async fn add_parts( let mut chat_id_blocked = Blocked::Not; let mut better_msg = None; - let mut group_changes_msgs = (Vec::new(), None); + let mut group_changes = GroupChangesInfo::default(); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await); } @@ -755,10 +761,14 @@ async fn add_parts( ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default(); let allow_creation; - if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage + if mime_parser.decrypting_failed { + allow_creation = false; + } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage && is_dc_message == MessengerMessage::No + && !context.get_config_bool(Config::IsChatmail).await? { - // this message is a classic email not a chat-message nor a reply to one + // the message is a classic email in a classic profile + // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported) match show_emails { ShowEmails::Off => { info!(context, "Classical email not shown (TRASH)."); @@ -778,7 +788,7 @@ async fn add_parts( // (of course, the user can add other chats manually later) let to_id: ContactId; let state: MessageState; - let mut hidden = false; + let mut hidden = is_reaction; let mut needs_delete_job = false; let mut restore_protection = false; @@ -915,7 +925,7 @@ async fn add_parts( } } - group_changes_msgs = apply_group_changes( + group_changes = apply_group_changes( context, mime_parser, group_chat_id, @@ -1033,11 +1043,8 @@ async fn add_parts( } } - state = if seen - || fetching_existing_messages - || is_mdn - || is_reaction - || chat_id_blocked == Blocked::Yes + state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent + // No check for `hidden` because only reactions are such and they should be `InFresh`. { MessageState::InSeen } else { @@ -1103,7 +1110,7 @@ async fn add_parts( } } - if mime_parser.decrypting_failed && !fetching_existing_messages { + if mime_parser.decrypting_failed { if chat_id.is_none() { chat_id = Some(DC_CHAT_ID_TRASH); } else { @@ -1185,7 +1192,7 @@ async fn add_parts( } if let Some(chat_id) = chat_id { - group_changes_msgs = apply_group_changes( + group_changes = apply_group_changes( context, mime_parser, chat_id, @@ -1229,12 +1236,6 @@ async fn add_parts( } } - if fetching_existing_messages && mime_parser.decrypting_failed { - chat_id = Some(DC_CHAT_ID_TRASH); - // We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats. - info!(context, "Existing non-decipherable message (TRASH)."); - } - if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 { if let Some(part) = mime_parser.parts.first() { if part.typ == Viewtype::Text && part.msg.is_empty() { @@ -1246,14 +1247,10 @@ async fn add_parts( } let orig_chat_id = chat_id; - let mut chat_id = if is_reaction { + let mut chat_id = chat_id.unwrap_or_else(|| { + info!(context, "No chat id for message (TRASH)."); DC_CHAT_ID_TRASH - } else { - chat_id.unwrap_or_else(|| { - info!(context, "No chat id for message (TRASH)."); - DC_CHAT_ID_TRASH - }) - }; + }); // Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded. let mut ephemeral_timer = if is_partial_download.is_some() { @@ -1292,8 +1289,18 @@ async fn add_parts( && !mime_parser.parts.is_empty() && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer { + let chat_contacts = + HashSet::::from_iter(chat::get_chat_contacts(context, chat_id).await?); + let is_from_in_chat = + !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id); + info!(context, "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."); - if is_dc_message == MessengerMessage::Yes + if !is_from_in_chat { + warn!( + context, + "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.", + ); + } else if is_dc_message == MessengerMessage::Yes && get_previous_message(context, mime_parser) .await? .map(|p| p.ephemeral_timer) @@ -1382,22 +1389,14 @@ async fn add_parts( } } - // Ensure replies to messages are sorted after the parent message. - // - // This is useful in a case where sender clocks are not - // synchronized and parent message has a Date: header with a - // timestamp higher than reply timestamp. - // - // This does not help if parent message arrives later than the - // reply. - let parent_timestamp = mime_parser.get_parent_timestamp(context).await?; - let sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| { - std::cmp::max(sort_timestamp, parent_timestamp) - }); - - // if the mime-headers should be saved, find out its size - // (the mime-header ends with an empty line) - let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?; + let sort_timestamp = tweak_sort_timestamp( + context, + mime_parser, + group_changes.silent, + chat_id, + sort_timestamp, + ) + .await?; let mime_in_reply_to = mime_parser .get_header(HeaderDef::InReplyTo) @@ -1424,7 +1423,7 @@ async fn add_parts( // `true` finally. let mut save_mime_modified = false; - let mime_headers = if save_mime_headers || mime_parser.is_mime_modified { + let mime_headers = if mime_parser.is_mime_modified { let headers = if !mime_parser.decoded_data.is_empty() { mime_parser.decoded_data.clone() } else { @@ -1437,28 +1436,28 @@ async fn add_parts( let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len()); - if let Some(msg) = group_changes_msgs.1 { + if let Some(m) = group_changes.better_msg { match &better_msg { - None => better_msg = Some(msg), + None => better_msg = Some(m), Some(_) => { - if !msg.is_empty() { - group_changes_msgs.0.push(msg) + if !m.is_empty() { + group_changes.extra_msgs.push((m, is_system_message, None)) } } } } - for group_changes_msg in group_changes_msgs.0 { - // Currently all additional group changes messages are "Member added". + for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs { chat::add_info_msg_with_cmd( context, chat_id, &group_changes_msg, - SystemMessage::MemberAddedToGroup, + cmd, sort_timestamp, None, None, None, + added_removed_id, ) .await?; } @@ -1490,11 +1489,16 @@ async fn add_parts( } } + if handle_edit_delete(context, mime_parser, from_id).await? { + chat_id = DC_CHAT_ID_TRASH; + info!(context, "Message edits/deletes existing message (TRASH)."); + } + let mut parts = mime_parser.parts.iter().peekable(); while let Some(part) = parts.next() { if part.is_reaction { let reaction_str = simplify::remove_footers(part.msg.as_str()); - let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages; + let is_incoming_fresh = mime_parser.incoming && !seen; set_msg_reaction( context, mime_in_reply_to, @@ -1526,7 +1530,6 @@ async fn add_parts( } } - let mut txt_raw = "".to_string(); let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg { if better_msg.is_empty() && is_partial_download.is_none() { chat_id = DC_CHAT_ID_TRASH; @@ -1538,14 +1541,13 @@ async fn add_parts( let part_is_empty = typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none(); + if let Some(contact_id) = group_changes.added_removed_id { + param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string()); + } + save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden; let save_mime_modified = save_mime_modified && parts.peek().is_none(); - if part.typ == Viewtype::Text { - let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); - txt_raw = format!("{subject}\n\n{msg_raw}"); - } - let ephemeral_timestamp = if in_fresh { 0 } else { @@ -1572,7 +1574,7 @@ INSERT INTO msgs rfc724_mid, chat_id, from_id, to_id, timestamp, timestamp_sent, timestamp_rcvd, type, state, msgrmsg, - txt, txt_normalized, subject, txt_raw, param, hidden, + txt, txt_normalized, subject, param, hidden, bytes, mime_headers, mime_compressed, mime_in_reply_to, mime_references, mime_modified, error, ephemeral_timer, ephemeral_timestamp, download_state, hop_info @@ -1581,7 +1583,7 @@ INSERT INTO msgs ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ? @@ -1591,7 +1593,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id, from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent, type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg, txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject, - txt_raw=excluded.txt_raw, param=excluded.param, + param=excluded.param, hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to, mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer, @@ -1610,11 +1612,9 @@ RETURNING id typ, state, is_dc_message, - if trash { "" } else { msg }, - if trash { None } else { message::normalize_text(msg) }, - if trash { "" } else { &subject }, - // txt_raw might contain invalid utf8 - if trash { "" } else { &txt_raw }, + if trash || hidden { "" } else { msg }, + if trash || hidden { None } else { message::normalize_text(msg) }, + if trash || hidden { "" } else { &subject }, if trash { "".to_string() } else { @@ -1622,7 +1622,7 @@ RETURNING id }, hidden, part.bytes as isize, - if (save_mime_headers || save_mime_modified) && !trash { + if save_mime_modified && !(trash || hidden) { mime_headers.clone() } else { Vec::new() @@ -1682,9 +1682,7 @@ RETURNING id context, part.typ, chat_id, - part.param - .get_path(Param::File, context) - .unwrap_or_default(), + part.param.get(Param::Filename), *msg_id, ) .await?; @@ -1711,7 +1709,7 @@ RETURNING id "Message has {icnt} parts and is assigned to chat #{chat_id}." ); - if !chat_id.is_trash() { + if !chat_id.is_trash() && !hidden { let mut chat = Chat::load_from_db(context, chat_id).await?; // In contrast to most other update-timestamps, @@ -1749,6 +1747,7 @@ RETURNING id Ok(ReceivedMsg { chat_id, state, + hidden, sort_timestamp, msg_ids: created_db_entries, needs_delete_job, @@ -1757,6 +1756,123 @@ RETURNING id }) } +/// Checks for "Chat-Edit" and "Chat-Delete" headers, +/// and edits/deletes existing messages accordingly. +/// +/// Returns `true` if this message is an edit/deletion request. +async fn handle_edit_delete( + context: &Context, + mime_parser: &MimeMessage, + from_id: ContactId, +) -> Result { + if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) { + if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? { + if let Some(mut original_msg) = + Message::load_from_db_optional(context, original_msg_id).await? + { + if original_msg.from_id == from_id { + if let Some(part) = mime_parser.parts.first() { + let edit_msg_showpadlock = part + .param + .get_bool(Param::GuaranteeE2ee) + .unwrap_or_default(); + if edit_msg_showpadlock || !original_msg.get_showpadlock() { + let new_text = + part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg); + chat::save_text_edit_to_db(context, &mut original_msg, new_text) + .await?; + } else { + warn!(context, "Edit message: Not encrypted."); + } + } + } else { + warn!(context, "Edit message: Bad sender."); + } + } else { + warn!(context, "Edit message: Database entry does not exist."); + } + } else { + warn!( + context, + "Edit message: rfc724_mid {rfc724_mid:?} not found." + ); + } + + Ok(true) + } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) { + if let Some(part) = mime_parser.parts.first() { + // See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted + // deletion requests, so there's no need to support them. + if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) { + let mut modified_chat_ids = HashSet::new(); + let mut msg_ids = Vec::new(); + + let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect(); + for rfc724_mid in rfc724_mid_vec { + if let Some((msg_id, _)) = + message::rfc724_mid_exists(context, rfc724_mid).await? + { + if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? { + if msg.from_id == from_id { + message::delete_msg_locally(context, &msg).await?; + msg_ids.push(msg.id); + modified_chat_ids.insert(msg.chat_id); + } else { + warn!(context, "Delete message: Bad sender."); + } + } else { + warn!(context, "Delete message: Database entry does not exist."); + } + } else { + warn!(context, "Delete message: {rfc724_mid:?} not found."); + } + } + message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?; + } else { + warn!(context, "Delete message: Not encrypted."); + } + } + + Ok(true) + } else { + Ok(false) + } +} + +async fn tweak_sort_timestamp( + context: &Context, + mime_parser: &mut MimeMessage, + silent: bool, + chat_id: ChatId, + sort_timestamp: i64, +) -> Result { + // Ensure replies to messages are sorted after the parent message. + // + // This is useful in a case where sender clocks are not + // synchronized and parent message has a Date: header with a + // timestamp higher than reply timestamp. + // + // This does not help if parent message arrives later than the + // reply. + let parent_timestamp = mime_parser.get_parent_timestamp(context).await?; + let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| { + std::cmp::max(sort_timestamp, parent_timestamp) + }); + + // If the message should be silent, + // set the timestamp to be no more than the same as last message + // so that the chat is not sorted to the top of the chatlist. + if silent { + let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? { + t + } else { + chat_id.created_timestamp(context).await? + }; + sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp); + } + Ok(sort_timestamp) +} + /// Saves attached locations to the database. /// /// Emits an event if at least one new location was added. @@ -2205,11 +2321,25 @@ async fn update_chats_contacts_timestamps( Ok(modified) } +/// The return type of [apply_group_changes]. +/// Contains information on which system messages +/// should be shown in the chat. +#[derive(Default)] +struct GroupChangesInfo { + /// Optional: A better message that should replace the original system message. + /// If this is an empty string, the original system message should be trashed. + better_msg: Option, + /// Added/removed contact `better_msg` refers to. + added_removed_id: Option, + /// If true, the user should not be notified about the group change. + silent: bool, + /// A list of additional group changes messages that should be shown in the chat. + extra_msgs: Vec<(String, SystemMessage, Option)>, +} + /// Apply group member list, name, avatar and protection status changes from the MIME message. /// -/// Returns `Vec` of group changes messages and, optionally, a better message to replace the -/// original system message. If the better message is empty, the original system message -/// should be trashed. +/// Returns [GroupChangesInfo]. /// /// * `to_ids` - contents of the `To` and `Cc` headers. /// * `past_ids` - contents of the `Chat-Group-Past-Members` header. @@ -2221,19 +2351,20 @@ async fn apply_group_changes( to_ids: &[ContactId], past_ids: &[ContactId], verified_encryption: &VerifiedEncryption, -) -> Result<(Vec, Option)> { +) -> Result { if chat_id.is_special() { // Do not apply group changes to the trash chat. - return Ok((Vec::new(), None)); + return Ok(GroupChangesInfo::default()); } let mut chat = Chat::load_from_db(context, chat_id).await?; if chat.typ != Chattype::Group { - return Ok((Vec::new(), None)); + return Ok(GroupChangesInfo::default()); } let mut send_event_chat_modified = false; let (mut removed_id, mut added_id) = (None, None); let mut better_msg = None; + let mut silent = false; // True if a Delta Chat client has explicitly added our current primary address. let self_added = @@ -2271,6 +2402,7 @@ async fn apply_group_changes( removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?; if let Some(id) = removed_id { better_msg = if id == from_id { + silent = true; Some(stock_str::msg_group_left_local(context, from_id).await) } else { Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await) @@ -2288,9 +2420,19 @@ async fn apply_group_changes( } better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await); - } else if let Some(old_name) = mime_parser + } + + let group_name_timestamp = mime_parser + .get_header(HeaderDef::ChatGroupNameTimestamp) + .and_then(|s| s.parse::().ok()); + if let Some(old_name) = mime_parser .get_header(HeaderDef::ChatGroupNameChanged) .map(|s| s.trim()) + .or(match group_name_timestamp { + Some(0) => None, + Some(_) => Some(chat.name.as_str()), + None => None, + }) { if let Some(grpname) = mime_parser .get_header(HeaderDef::ChatGroupName) @@ -2298,14 +2440,16 @@ async fn apply_group_changes( .filter(|grpname| grpname.len() < 200) { let grpname = &sanitize_single_line(grpname); - let old_name = &sanitize_single_line(old_name); - if chat_id - .update_timestamp( - context, - Param::GroupNameTimestamp, - mime_parser.timestamp_sent, - ) - .await? + + let chat_group_name_timestamp = + chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0); + let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent); + // To provide group name consistency, compare names if timestamps are equal. + if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name) + && chat_id + .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp) + .await? + && grpname != &chat.name { info!(context, "Updating grpname for chat {chat_id}."); context @@ -2314,10 +2458,19 @@ async fn apply_group_changes( .await?; send_event_chat_modified = true; } - - better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await); + if mime_parser + .get_header(HeaderDef::ChatGroupNameChanged) + .is_some() + { + let old_name = &sanitize_single_line(old_name); + better_msg.get_or_insert( + stock_str::msg_grp_name(context, old_name, grpname, from_id).await, + ); + } } - } else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) { + } + + if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) { if value == "group-avatar-changed" { if let Some(avatar_action) = &mime_parser.group_avatar { // this is just an explicit message containing the group-avatar, @@ -2497,7 +2650,16 @@ async fn apply_group_changes( context.emit_event(EventType::ChatModified(chat_id)); chatlist_events::emit_chatlist_item_changed(context, chat_id); } - Ok((group_changes_msgs, better_msg)) + Ok(GroupChangesInfo { + better_msg, + added_removed_id: if added_id.is_some() { + added_id + } else { + removed_id + }, + silent, + extra_msgs: group_changes_msgs, + }) } /// Returns a list of strings that should be shown as info messages, informing about group membership changes. @@ -2506,7 +2668,7 @@ async fn group_changes_msgs( added_ids: &HashSet, removed_ids: &HashSet, chat_id: ChatId, -) -> Result> { +) -> Result)>> { let mut group_changes_msgs = Vec::new(); if !added_ids.is_empty() { warn!( @@ -2523,23 +2685,27 @@ async fn group_changes_msgs( group_changes_msgs.reserve(added_ids.len() + removed_ids.len()); for contact_id in added_ids { let contact = Contact::get_by_id(context, *contact_id).await?; - group_changes_msgs.push( + group_changes_msgs.push(( stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED) .await, - ); + SystemMessage::MemberAddedToGroup, + Some(contact.id), + )); } for contact_id in removed_ids { let contact = Contact::get_by_id(context, *contact_id).await?; - group_changes_msgs.push( + group_changes_msgs.push(( stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED) .await, - ); + SystemMessage::MemberRemovedFromGroup, + Some(contact.id), + )); } Ok(group_changes_msgs) } -static LIST_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap()); +static LIST_ID_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap()); fn mailinglist_header_listid(list_id_header: &str) -> Result { Ok(match LIST_ID_REGEX.captures(list_id_header) { @@ -2647,8 +2813,8 @@ fn compute_mailinglist_name( // (as that part is much more visible, we assume, that names is shorter and comes more to the point, // than the sometimes longer part from ListId) let subject = mime_parser.get_subject().unwrap_or_default(); - static SUBJECT: Lazy = - Lazy::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name + static SUBJECT: LazyLock = + LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name if let Some(cap) = SUBJECT.captures(&subject) { name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str()); } @@ -2675,8 +2841,8 @@ fn compute_mailinglist_name( // but strip some known, long hash prefixes if name.is_empty() { // 51231231231231231231231232869f58.xing.com -> xing.com - static PREFIX_32_CHARS_HEX: Lazy = - Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap()); + static PREFIX_32_CHARS_HEX: LazyLock = + LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap()); if let Some(cap) = PREFIX_32_CHARS_HEX .captures(listid) .and_then(|caps| caps.get(2)) @@ -2893,23 +3059,12 @@ async fn update_verified_keys( /// Checks whether the message is allowed to appear in a protected chat. /// /// This means that it is encrypted and signed with a verified key. -/// -/// Also propagates gossiped keys to verified if needed. -async fn has_verified_encryption( - context: &Context, +fn has_verified_encryption( mimeparser: &MimeMessage, from_id: ContactId, - to_ids: &[ContactId], ) -> Result { use VerifiedEncryption::*; - // We do not need to check if we are verified with ourself. - let to_ids = to_ids - .iter() - .copied() - .filter(|id| *id != ContactId::SELF) - .collect::>(); - if !mimeparser.was_encrypted() { return Ok(NotVerified("This message is not encrypted".to_string())); }; @@ -2938,21 +3093,24 @@ async fn has_verified_encryption( } } - mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?; Ok(Verified) } async fn mark_recipients_as_verified( context: &Context, from_id: ContactId, - to_ids: Vec, + to_ids: &[ContactId], mimeparser: &MimeMessage, ) -> Result<()> { if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } let contact = Contact::get_by_id(context, from_id).await?; - for id in to_ids { + for &id in to_ids { + if id == ContactId::SELF { + continue; + } + let Some((to_addr, is_verified)) = context .sql .query_row_optional( diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 409ea9937a..5b1ce99e65 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -1,3 +1,4 @@ +use rand::Rng; use std::time::Duration; use tokio::fs; @@ -903,7 +904,7 @@ async fn test_concat_multiple_ndns() -> Result<()> { let raw = include_str!("../../test-data/message/posteo_ndn.eml"); let raw = raw.replace( "Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>", - &format!("Message-ID: <{}>", mid), + &format!("Message-ID: <{mid}>"), ); receive_imf(&t, raw.as_bytes(), false).await?; @@ -1061,8 +1062,8 @@ async fn test_classic_mailing_list() -> Result<()> { let mime = sent.payload(); println!("Sent mime message is:\n\n{mime}\n\n"); - assert!(mime.contains("Content-Type: text/plain; charset=utf-8\r\n")); - assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); + assert!(mime.contains("Content-Type: text/plain; charset=\"utf-8\"\r\n")); + assert!(mime.contains("Subject: Re: [delta-dev] What's up?\r\n")); assert!(mime.contains("MIME-Version: 1.0\r\n")); assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); assert!(mime.contains("Chat-Version: 1.0\r\n")); @@ -1894,44 +1895,11 @@ async fn test_save_mime_headers_off() -> anyhow::Result<()> { let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; assert_eq!(msg.get_text(), "hi!"); - assert!(!msg.get_showpadlock()); let mime = message::get_mime_headers(&bob, msg.id).await?; assert!(mime.is_empty()); Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_save_mime_headers_on() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - alice.set_config_bool(Config::SaveMimeHeaders, true).await?; - let bob = TestContext::new_bob().await; - bob.set_config_bool(Config::SaveMimeHeaders, true).await?; - - // alice sends a message to bob, bob sees full mime - let chat_alice = alice.create_chat(&bob).await; - chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), "hi!"); - assert!(!msg.get_showpadlock()); - let mime = message::get_mime_headers(&bob, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - - // another one, from bob to alice, that gets encrypted - let chat_bob = bob.create_chat(&alice).await; - chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; - let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), "ho!"); - assert!(msg.get_showpadlock()); - let mime = message::get_mime_headers(&alice, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - Ok(()) -} - async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: bool) { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; @@ -2250,6 +2218,17 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> { let mut msg = Message::new_text("Happy birthday to me".to_string()); chat::send_msg(bob, chat_id, &mut msg).await?; assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); + + bob.set_config_bool(Config::BccSelf, true).await?; + bob.set_config(Config::DeleteServerAfter, Some("1")).await?; + let mut msg = Message::new_text("Happy birthday to me".to_string()); + chat::send_msg(bob, chat_id, &mut msg).await?; + assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none()); + + bob.set_config(Config::DeleteServerAfter, None).await?; + let mut msg = Message::new_text("Happy birthday to me".to_string()); + chat::send_msg(bob, chat_id, &mut msg).await?; + assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some()); Ok(()) } @@ -3134,7 +3113,6 @@ Message with references."#; async fn test_rfc1847_encapsulation() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - alice.configure_addr("alice@example.org").await; // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. let chat_alice = alice.create_chat(&bob).await; @@ -3334,6 +3312,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { let alice1 = tcm.alice().await; let alice2 = tcm.alice().await; let bob = tcm.bob().await; + let charlie = tcm.charlie().await; // =============== Bob creates a group =============== let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; @@ -3342,8 +3321,8 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { time(), group_id, &[ - bob.add_or_lookup_contact(&alice1).await.id, - Contact::create(&bob, "", "charlie@example.org").await?, + bob.add_or_lookup_contact_id(&alice1).await, + bob.add_or_lookup_contact_id(&charlie).await, ], ) .await?; @@ -3470,8 +3449,7 @@ async fn test_send_as_bot() -> Result<()> { let alice = &tcm.alice().await; alice.set_config(Config::Bot, Some("1")).await.unwrap(); let bob = &tcm.bob().await; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let alice_bob_id = Contact::create(alice, "", &bob_addr).await?; + let alice_bob_id = alice.add_or_lookup_contact_id(bob).await; let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; let alice_chat_id = ChatId::lookup_by_contact(alice, alice_bob_id) .await? @@ -3487,46 +3465,6 @@ async fn test_send_as_bot() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_bot_recv_existing_msg() -> Result<()> { - let mut tcm = TestContextManager::new(); - let bob = &tcm.bob().await; - bob.set_config(Config::Bot, Some("1")).await.unwrap(); - bob.set_config(Config::FetchExistingMsgs, Some("1")) - .await - .unwrap(); - let fetching_existing_messages = true; - let msg = receive_imf_from_inbox( - bob, - "first@example.org", - b"From: Alice \n\ - To: Bob \n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - Date: Sun, 14 Nov 2021 00:10:00 +0000\n\ - Content-Type: text/plain\n\ - \n\ - hello\n", - false, - None, - fetching_existing_messages, - ) - .await? - .unwrap(); - let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?; - assert_eq!(msg.state, MessageState::InFresh); - let event = bob - .evtracker - .get_matching(|ev| matches!(ev, EventType::IncomingMsg { .. })) - .await; - let EventType::IncomingMsg { chat_id, msg_id } = event else { - unreachable!(); - }; - assert_eq!(chat_id, msg.chat_id); - assert_eq!(msg_id, msg.id); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_wrong_date_in_imf_section() { let mut tcm = TestContextManager::new(); @@ -4065,7 +4003,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> { 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 bob_id = alice.add_or_lookup_contact_id(bob).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?; @@ -4087,38 +4025,39 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sync_member_list_on_rejoin() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; - let claire_id = Contact::create(&alice, "", "claire@example.de").await?; + let bob_id = alice.add_or_lookup_contact_id(bob).await; + let fiona_id = alice.add_or_lookup_contact_id(fiona).await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?; - add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; - add_contact_to_chat(&alice, alice_chat_id, claire_id).await?; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?; + add_contact_to_chat(alice, alice_chat_id, bob_id).await?; + add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; let add = alice.pop_sent_msg().await; - let bob = tcm.bob().await; bob.recv_msg(&add).await; let bob_chat_id = bob.get_last_msg().await.chat_id; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3); + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3); // remove bob from chat - remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?; + remove_contact_from_chat(alice, alice_chat_id, bob_id).await?; let remove_bob = alice.pop_sent_msg().await; bob.recv_msg(&remove_bob).await; // remove any other member - remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?; + remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?; alice.pop_sent_msg().await; // re-add bob - add_contact_to_chat(&alice, alice_chat_id, bob_id).await?; + add_contact_to_chat(alice, alice_chat_id, bob_id).await?; let add2 = alice.pop_sent_msg().await; bob.recv_msg(&add2).await; // number of members in chat should have updated - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); Ok(()) } @@ -4130,8 +4069,7 @@ 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_bob_id = alice.add_or_lookup_contact_id(bob).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?; // Alice creates a group chat. Bob accepts it. @@ -4174,107 +4112,101 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; + let charlie = &tcm.charlie().await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "bob", "bob@example.net").await?, + alice.add_or_lookup_contact_id(bob).await, ) .await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; - bob_chat_id.accept(&bob).await?; + bob_chat_id.accept(bob).await?; // alice adds a member add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "fiona", "fiona@example.net").await?, + alice.add_or_lookup_contact_id(fiona).await, ) .await?; - // bob adds a member. - let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?; - add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?; + // Bob adds a member. + let bob_charlie = bob.add_or_lookup_contact_id(charlie).await; + add_contact_to_chat(bob, bob_chat_id, bob_charlie).await?; alice.recv_msg(&bob.pop_sent_msg().await).await; // Bob didn't receive the addition of Fiona, but Alice mustn't remove Fiona from the members // list back. Instead, Bob must add Fiona from the next Alice's message to make their group // members view consistent. - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4); + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 4); // Just a dumb check for remove_contact_from_chat(). Let's have it in this only place. - remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?; + remove_contact_from_chat(bob, bob_chat_id, bob_charlie).await?; alice.recv_msg(&bob.pop_sent_msg().await).await; - assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3); + assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3); SystemTime::shift(Duration::from_secs(3600)); - send_text_msg( - &alice, - alice_chat_id, - "Finally add Fiona please".to_string(), - ) - .await?; + send_text_msg(alice, alice_chat_id, "Finally add Fiona please".to_string()).await?; bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3); + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delayed_removal_is_ignored() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - let alice_fiona = Contact::create(&alice, "fiona", "fiona@example.net").await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_bob = alice.add_or_lookup_contact_id(bob).await; + let alice_fiona = alice.add_or_lookup_contact_id(fiona).await; // create chat with three members - add_to_chat_contacts_table( - &alice, - time(), - chat_id, - &[ - Contact::create(&alice, "bob", "bob@example.net").await?, - alice_fiona, - ], - ) - .await?; + add_to_chat_contacts_table(alice, time(), chat_id, &[alice_bob, alice_fiona]).await?; - send_text_msg(&alice, chat_id, "populate".to_string()).await?; + send_text_msg(alice, chat_id, "populate".to_string()).await?; let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; - bob_chat_id.accept(&bob).await?; + bob_chat_id.accept(bob).await?; // Bob removes Fiona. - let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?; - remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?; + let bob_contact_fiona = bob.add_or_lookup_contact_id(fiona).await; + remove_contact_from_chat(bob, bob_chat_id, bob_contact_fiona).await?; let remove_msg = bob.pop_sent_msg().await; - // Bob adds new members "blue" and "orange", but first addition message is lost. - let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?; - add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?; + // Bob adds new members Dom and Elena, but first addition message is lost. + let dom = &tcm.dom().await; + let elena = &tcm.elena().await; + let bob_dom = bob.add_or_lookup_contact_id(dom).await; + add_contact_to_chat(bob, bob_chat_id, bob_dom).await?; bob.pop_sent_msg().await; - let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?; - add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?; + let bob_elena = bob.add_or_lookup_contact_id(elena).await; + add_contact_to_chat(bob, bob_chat_id, bob_elena).await?; let add_msg = bob.pop_sent_msg().await; // Alice only receives the second member addition, // but this results in addition of both members // and removal of Fiona. alice.recv_msg(&add_msg).await; - assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4); + assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 4); // Alice re-adds Fiona. - add_contact_to_chat(&alice, chat_id, alice_fiona).await?; - assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5); + add_contact_to_chat(alice, chat_id, alice_fiona).await?; + assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5); // Delayed removal of Fiona by Bob shouldn't remove her. alice.recv_msg(&remove_msg).await; - assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5); + assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5); alice .golden_test_chat(chat_id, "receive_imf_delayed_removal_is_ignored") @@ -4286,41 +4218,42 @@ async fn test_delayed_removal_is_ignored() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_dont_readd_with_normal_msg() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "bob", "bob@example.net").await?, + alice.add_or_lookup_contact_id(bob).await, ) .await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; - bob_chat_id.accept(&bob).await?; + bob_chat_id.accept(bob).await?; // Bob leaves, but Alice didn't receive Bob's leave message. - remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; + remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?; bob.pop_sent_msg().await; - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1); + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 1); SystemTime::shift(Duration::from_secs(3600)); add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "fiora", "fiora@example.net").await?, + alice.add_or_lookup_contact_id(fiona).await, ) .await?; bob.recv_msg(&alice.pop_sent_msg().await).await; // Bob received a message from Alice, but this should not re-add him to the group. - assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?); - // Bob got an update that fiora is added nevertheless. - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); + // Bob got an update that Fiona is added nevertheless. + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); Ok(()) } @@ -4524,16 +4457,17 @@ async fn test_mua_can_readd() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_member_left_does_not_create_chat() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + alice.add_or_lookup_contact_id(bob).await, ) .await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; alice.pop_sent_msg().await; // Bob only received a message of Alice leaving the group. @@ -4543,7 +4477,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> { // especially the chats that were created due to "split group" bugs // which some members simply deleted and some members left, // recreating the chat for others. - remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?; + remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?; bob.recv_msg_trash(&alice.pop_sent_msg().await).await; Ok(()) @@ -4551,44 +4485,45 @@ async fn test_member_left_does_not_create_chat() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; add_contact_to_chat( - &alice, + alice, alice_chat_id, - Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + alice.add_or_lookup_contact_id(bob).await, ) .await?; - send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; alice.pop_sent_msg().await; - send_text_msg(&alice, alice_chat_id, "second message".to_string()).await?; + send_text_msg(alice, alice_chat_id, "second message".to_string()).await?; let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; assert!(!bob_chat_id.is_special()); // Bob missed the message adding them, but must recreate the member list. - assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); - assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + assert!(is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?); // But if Bob just left, they mustn't recreate the member list even after missing a message. - bob_chat_id.accept(&bob).await?; - remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?; + bob_chat_id.accept(bob).await?; + remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?; bob.pop_sent_msg().await; - send_text_msg(&alice, alice_chat_id, "3rd message".to_string()).await?; + send_text_msg(alice, alice_chat_id, "3rd message".to_string()).await?; alice.pop_sent_msg().await; - send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?; + send_text_msg(alice, alice_chat_id, "4th message".to_string()).await?; bob.recv_msg(&alice.pop_sent_msg().await).await; - assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?); // Even if some time passed, Bob must not be re-added back. SystemTime::shift(Duration::from_secs(3600)); - send_text_msg(&alice, alice_chat_id, "5th message".to_string()).await?; + send_text_msg(alice, alice_chat_id, "5th message".to_string()).await?; alice.pop_sent_msg().await; - send_text_msg(&alice, alice_chat_id, "6th message".to_string()).await?; + send_text_msg(alice, alice_chat_id, "6th message".to_string()).await?; bob.recv_msg(&alice.pop_sent_msg().await).await; - assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); + assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?); Ok(()) } @@ -4601,7 +4536,7 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> { add_contact_to_chat( &alice, alice_chat_id, - Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?, + alice.add_or_lookup_contact_id(&bob).await, ) .await?; send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?; @@ -4611,12 +4546,7 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> { add_contact_to_chat( &alice, alice_chat_id, - Contact::create( - &alice, - "fiona", - &fiona.get_config(Config::Addr).await?.unwrap(), - ) - .await?, + alice.add_or_lookup_contact_id(&fiona).await, ) .await?; let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id; @@ -4628,12 +4558,7 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> { // Bob missed the message adding fiona, but mustn't recreate the member list. assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2); assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?); - let bob_alice_contact = Contact::create( - &bob, - "alice", - &alice.get_config(Config::Addr).await?.unwrap(), - ) - .await?; + let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await; assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?); Ok(()) } @@ -4674,7 +4599,14 @@ async fn test_download_later() -> Result<()> { let bob = tcm.bob().await; let bob_chat = bob.create_chat(&alice).await; - let text = String::from_utf8(vec![b'a'; MIN_DOWNLOAD_LIMIT as usize])?; + + // Generate a random string so OpenPGP does not compress it. + let text: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(MIN_DOWNLOAD_LIMIT as usize) + .map(char::from) + .collect(); + let sent_msg = bob.send_text(bob_chat.id, &text).await; let msg = alice.recv_msg(&sent_msg).await; assert_eq!(msg.download_state, DownloadState::Available); @@ -4705,8 +4637,10 @@ async fn test_outgoing_msg_forgery() -> Result<()> { // 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; + assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 0); bob.configure_addr("bob@example.net").await; imex(bob, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; + assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1); let malice = &TestContext::new().await; malice.configure_addr(alice_addr).await; @@ -4714,6 +4648,7 @@ async fn test_outgoing_msg_forgery() -> Result<()> { .send_recv_accept(bob, malice, "hi from bob") .await .chat_id; + assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1); let sent_msg = malice.send_text(malice_chat_id, "hi from malice").await; let msg = alice.recv_msg(&sent_msg).await; @@ -4809,13 +4744,14 @@ async fn test_create_group_with_big_msg() -> Result<()> { async fn test_partial_group_consistency() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; - let bob_id = Contact::create(&alice, "", "bob@example.net").await?; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + let bob_id = alice.add_or_lookup_contact_id(&bob).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 add = alice.pop_sent_msg().await; - let bob = tcm.bob().await; bob.recv_msg(&add).await; let bob_chat_id = bob.get_last_msg().await.chat_id; let contacts = get_chat_contacts(&bob, bob_chat_id).await?; @@ -4835,7 +4771,6 @@ Content-Type: text/plain Chat-Group-Member-Added: charlie@example.com", false, Some(100000), - false, ) .await? .context("no received message")?; @@ -4850,7 +4785,7 @@ Chat-Group-Member-Added: charlie@example.com", add_contact_to_chat( &alice, alice_chat_id, - Contact::create(&alice, "fiona", "fiona@example.net").await?, + alice.add_or_lookup_contact_id(&fiona).await, ) .await?; @@ -4873,7 +4808,6 @@ Content-Type: text/plain Chat-Group-Member-Added: charlie@example.com", false, None, - false, ) .await? .context("no received message")?; @@ -4935,11 +4869,10 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap(); mark_as_verified(alice, fiona).await; let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id; - assert!(add_contact_to_chat(alice, group_id, alice_fiona_id) - .await - .is_err()); - // Sending the message failed, - // but member is added to the chat locally already. + add_contact_to_chat(alice, group_id, alice_fiona_id).await?; + + // The message is not sent to Bob, + // but member is added to the chat locally anyway. assert!(is_contact_in_chat(alice, group_id, alice_fiona_id).await?); let msg = alice.get_last_msg_in(group_id).await; assert!(msg.is_info()); @@ -4948,10 +4881,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await ); - // Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that - // may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a - // normal scenario anyway. - remove_contact_from_chat(alice, group_id, alice_bob_id).await?; assert!(!is_contact_in_chat(alice, group_id, alice_bob_id).await?); let msg = alice.get_last_msg_in(group_id).await; @@ -4960,7 +4889,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { msg.get_text(), stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await ); - assert!(msg.error().is_some()); Ok(()) } @@ -5057,8 +4985,9 @@ async fn test_unarchive_on_member_removal() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let bob_id = Contact::create(alice, "", "bob@example.net").await?; - let fiona_id = Contact::create(alice, "", "fiona@example.net").await?; + let fiona = &tcm.fiona().await; + let bob_id = alice.add_or_lookup_contact_id(bob).await; + let fiona_id = alice.add_or_lookup_contact_id(fiona).await; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?; add_contact_to_chat(alice, alice_chat_id, bob_id).await?; add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; @@ -5161,11 +5090,11 @@ async fn test_references() -> Result<()> { alice.set_config_bool(Config::BccSelf, true).await?; let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; - let _sent = alice + 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?; + let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).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; @@ -5425,6 +5354,65 @@ Hello!" Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_rename_chat_on_missing_message() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let charlie = tcm.charlie().await; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_to_chat_contacts_table( + &alice, + time(), + chat_id, + &[alice.add_or_lookup_contact_id(&bob).await], + ) + .await?; + send_text_msg(&alice, chat_id, "populate".to_string()).await?; + let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id; + bob_chat_id.accept(&bob).await?; + + // Bob changes the group name. NB: If Bob does this too fast, it's not guaranteed that his group + // name wins because "Group-Name-Timestamp" may not increase. + SystemTime::shift(Duration::from_secs(3600)); + chat::set_chat_name(&bob, bob_chat_id, "Renamed").await?; + bob.pop_sent_msg().await; + + // Bob adds a new member. + let bob_charlie = bob.add_or_lookup_contact_id(&charlie).await; + add_contact_to_chat(&bob, bob_chat_id, bob_charlie).await?; + let add_msg = bob.pop_sent_msg().await; + + // Alice only receives the member addition. + alice.recv_msg(&add_msg).await; + let chat = Chat::load_from_db(&alice, chat_id).await?; + assert_eq!(chat.get_name(), "Renamed"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_rename_chat_after_creating_invite() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + for populate_before_securejoin in [false, true] { + let alice_chat_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?; + let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?; + + SystemTime::shift(Duration::from_secs(60)); + chat::set_chat_name(alice, alice_chat_id, "Renamed").await?; + if populate_before_securejoin { + send_text_msg(alice, alice_chat_id, "populate".to_string()).await?; + alice.pop_sent_msg().await; + } + + let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await; + let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?; + assert_eq!(bob_chat.get_name(), "Renamed"); + } + Ok(()) +} + /// Tests that creating a group /// is preferred over assigning message to existing /// chat based on `In-Reply-To` and `References`. @@ -5582,3 +5570,38 @@ async fn test_two_group_securejoins() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sanitize_filename_in_received() -> Result<()> { + let alice = &TestContext::new_alice().await; + let raw = b"Message-ID: Mr.XA6y3og8-az.WGbH9_dNcQx@testr +To: +From: \"=?utf-8?q??=\" +Content-Type: multipart/mixed; boundary=\"mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z\" + + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z +Content-Type: text/plain; charset=utf-8 + +-- +Sent with my Delta Chat Messenger: https://delta.chat + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z +Content-Type: text/html +Content-Disposition: attachment; filename=\"te\xE2\x80\xACst/../../test.H|TML\xE2\x80\xAC \" +Content-Transfer-Encoding: base64 + +PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z--"; + + let msg = receive_imf(alice, raw, false).await?.unwrap(); + let msg = Message::load_from_db(alice, msg.msg_ids[0]).await?; + + assert_eq!(msg.get_filename().unwrap(), "test.HTML"); + + let blob = msg.param.get_file_blob(alice)?.unwrap(); + assert_eq!(blob.suffix().unwrap(), "html"); + + Ok(()) +} diff --git a/src/release.rs b/src/release.rs index 0545079ec4..5b5cf6082e 100644 --- a/src/release.rs +++ b/src/release.rs @@ -1,10 +1,10 @@ //! DC release info. use chrono::NaiveDate; -use once_cell::sync::Lazy; +use std::sync::LazyLock; const DATE_STR: &str = include_str!("../release-date.in"); /// Last release date. -pub static DATE: Lazy = - Lazy::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap()); +pub static DATE: LazyLock = + LazyLock::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap()); diff --git a/src/scheduler.rs b/src/scheduler.rs index b0843509a3..6f0ca6d149 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -386,7 +386,7 @@ async fn inbox_loop( ) { use futures::future::FutureExt; - info!(ctx, "starting inbox loop"); + info!(ctx, "Starting inbox loop."); let ImapConnectionHandlers { mut connection, stop_receiver, @@ -396,7 +396,7 @@ async fn inbox_loop( let fut = async move { let ctx = ctx1; if let Err(()) = started.send(()) { - warn!(ctx, "inbox loop, missing started receiver"); + warn!(ctx, "Inbox loop, missing started receiver."); return; }; @@ -405,9 +405,10 @@ async fn inbox_loop( let session = if let Some(session) = old_session.take() { session } else { + info!(ctx, "Preparing new IMAP session for inbox."); match connection.prepare(&ctx).await { Err(err) => { - warn!(ctx, "Failed to prepare INBOX connection: {:#}.", err); + warn!(ctx, "Failed to prepare inbox connection: {err:#}."); continue; } Ok(session) => session, @@ -415,8 +416,12 @@ async fn inbox_loop( }; match inbox_fetch_idle(&ctx, &mut connection, session).await { - Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"), + Err(err) => warn!(ctx, "Failed inbox fetch_idle: {err:#}."), Ok(session) => { + info!( + ctx, + "IMAP loop iteration for inbox finished, keeping the session." + ); old_session = Some(session); } } @@ -426,7 +431,7 @@ async fn inbox_loop( stop_receiver .recv() .map(|_| { - info!(ctx, "shutting down inbox loop"); + info!(ctx, "Shutting down inbox loop."); }) .race(fut) .await; @@ -695,7 +700,7 @@ async fn simple_imap_loop( ) { use futures::future::FutureExt; - info!(ctx, "starting simple loop for {}", folder_meaning); + info!(ctx, "Starting simple loop for {folder_meaning}."); let ImapConnectionHandlers { mut connection, stop_receiver, @@ -706,7 +711,10 @@ async fn simple_imap_loop( let fut = async move { let ctx = ctx1; if let Err(()) = started.send(()) { - warn!(&ctx, "simple imap loop, missing started receiver"); + warn!( + ctx, + "Simple imap loop for {folder_meaning}, missing started receiver." + ); return; } @@ -715,6 +723,7 @@ async fn simple_imap_loop( let session = if let Some(session) = old_session.take() { session } else { + info!(ctx, "Preparing new IMAP session for {folder_meaning}."); match connection.prepare(&ctx).await { Err(err) => { warn!( @@ -730,6 +739,10 @@ async fn simple_imap_loop( match fetch_idle(&ctx, &mut connection, session, folder_meaning).await { Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"), Ok(session) => { + info!( + ctx, + "IMAP loop iteration for {folder_meaning} finished, keeping the session" + ); old_session = Some(session); } } @@ -739,7 +752,7 @@ async fn simple_imap_loop( stop_receiver .recv() .map(|_| { - info!(ctx, "shutting down simple loop"); + info!(ctx, "Shutting down IMAP loop for {folder_meaning}."); }) .race(fut) .await; diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index a9d24d4a9a..bd8ab9ab8d 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -241,7 +241,6 @@ pub(crate) async fn maybe_network_lost(context: &Context, stores: Vec Result { bob::start_protocol(context, invite).await } -/// Send handshake message from Alice's device; -/// Bob's handshake messages are sent in `BobState::send_handshake_message()`. +/// Send handshake message from Alice's device. async fn send_alice_handshake_msg( context: &Context, contact_id: ContactId, @@ -259,7 +256,7 @@ pub(crate) enum HandshakeMessage { /// This leaves it on the IMAP server. It means other devices on this account can /// receive and potentially process this message as well. This is useful for example /// when the other device is running the protocol and has the relevant QR-code - /// information while this device does not have the joiner state ([`BobState`]). + /// information while this device does not have the joiner state. Ignore, /// The message should be further processed by incoming message handling. /// @@ -281,7 +278,7 @@ pub(crate) enum HandshakeMessage { /// database; this is done by `receive_imf()` later on as needed. pub(crate) async fn handle_securejoin_handshake( context: &Context, - mime_message: &MimeMessage, + mime_message: &mut MimeMessage, contact_id: ContactId, ) -> Result { if contact_id.is_special() { @@ -337,12 +334,6 @@ pub(crate) async fn handle_securejoin_handshake( inviter_progress(context, contact_id, 300); - // for setup-contact, make Alice's one-to-one chat with Bob visible - // (secure-join-information are shown in the group chat) - if !join_vg { - ChatId::create_for_contact(context, contact_id).await?; - } - // Alice -> Bob send_alice_handshake_msg( context, @@ -383,14 +374,6 @@ pub(crate) async fn handle_securejoin_handshake( ); return Ok(HandshakeMessage::Ignore); } - if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? { - warn!( - context, - "Ignoring {step} message because of fingerprint mismatch." - ); - return Ok(HandshakeMessage::Ignore); - } - info!(context, "Fingerprint verified.",); // verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else { warn!( @@ -417,6 +400,14 @@ pub(crate) async fn handle_securejoin_handshake( } }; + if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? { + warn!( + context, + "Ignoring {step} message because of fingerprint mismatch." + ); + return Ok(HandshakeMessage::Ignore); + } + let contact_addr = Contact::get_by_id(context, contact_id) .await? .get_addr() @@ -436,9 +427,14 @@ pub(crate) async fn handle_securejoin_handshake( ); return Ok(HandshakeMessage::Ignore); } + info!(context, "Fingerprint verified via Auth code.",); contact_id.regossip_keys(context).await?; ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?; - info!(context, "Auth verified.",); + // for setup-contact, make Alice's one-to-one chat with Bob visible + // (secure-join-information are shown in the group chat) + if !join_vg { + ChatId::create_for_contact(context, contact_id).await?; + } context.emit_event(EventType::ContactsChanged(Some(contact_id))); inviter_progress(context, contact_id, 600); if let Some(group_chat_id) = group_chat_id { @@ -479,15 +475,10 @@ pub(crate) async fn handle_securejoin_handshake( ==== Step 7 in "Setup verified contact" protocol ==== =======================================================*/ "vc-contact-confirm" => { - if let Some(mut bobstate) = BobState::from_db(&context.sql).await? { - if !bobstate.is_msg_expected(context, step) { - warn!(context, "Unexpected vc-contact-confirm."); - return Ok(HandshakeMessage::Ignore); - } - - bobstate.step_contact_confirm(context).await?; - bobstate.emit_progress(context, JoinerProgress::Succeeded); - } + context.emit_event(EventType::SecurejoinJoinerProgress { + contact_id, + progress: JoinerProgress::Succeeded.to_usize(), + }); Ok(HandshakeMessage::Ignore) } "vg-member-added" => { @@ -506,15 +497,22 @@ pub(crate) async fn handle_securejoin_handshake( ); return Ok(HandshakeMessage::Propagate); } - if let Some(mut bobstate) = BobState::from_db(&context.sql).await? { - if !bobstate.is_msg_expected(context, step) { - warn!(context, "Unexpected vg-member-added."); - return Ok(HandshakeMessage::Propagate); - } - bobstate.step_contact_confirm(context).await?; - bobstate.emit_progress(context, JoinerProgress::Succeeded); + // Mark peer as backward verified. + // + // This is needed for the case when we join a non-protected group + // because in this case `Chat-Verified` header that otherwise + // sets backward verification is not sent. + if let Some(peerstate) = &mut mime_message.peerstate { + peerstate.backward_verified_key_id = + Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); + peerstate.save_to_db(&context.sql).await?; } + + context.emit_event(EventType::SecurejoinJoinerProgress { + contact_id, + progress: JoinerProgress::Succeeded.to_usize(), + }); Ok(HandshakeMessage::Propagate) } @@ -744,850 +742,4 @@ fn encrypted_and_signed( } #[cfg(test)] -mod tests { - use deltachat_contact_tools::{ContactAddress, EmailAddress}; - - use super::*; - use crate::chat::{remove_contact_from_chat, CantSendReason}; - use crate::chatlist::Chatlist; - use crate::constants::{self, Chattype}; - use crate::imex::{imex, ImexMode}; - use crate::receive_imf::receive_imf; - use crate::stock_str::{self, chat_protection_enabled}; - use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote}; - use crate::test_utils::{TestContext, TestContextManager}; - use crate::tools::SystemTime; - use std::collections::HashSet; - use std::time::Duration; - - #[derive(PartialEq)] - enum SetupContactCase { - Normal, - CheckProtectionTimestamp, - WrongAliceGossip, - SecurejoinWaitTimeout, - AliceIsBot, - AliceHasName, - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact() { - test_setup_contact_ex(SetupContactCase::Normal).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_protection_timestamp() { - test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_wrong_alice_gossip() { - test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_wait_timeout() { - test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_alice_is_bot() { - test_setup_contact_ex(SetupContactCase::AliceIsBot).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_alice_has_name() { - test_setup_contact_ex(SetupContactCase::AliceHasName).await - } - - async fn test_setup_contact_ex(case: SetupContactCase) { - let _n = TimeShiftFalsePositiveNote; - - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap(); - if case == SetupContactCase::AliceHasName { - alice - .set_config(Config::Displayname, Some("Alice")) - .await - .unwrap(); - } - let bob = tcm.bob().await; - bob.set_config(Config::Displayname, Some("Bob Examplenet")) - .await - .unwrap(); - let alice_auto_submitted_hdr; - match case { - SetupContactCase::AliceIsBot => { - alice.set_config_bool(Config::Bot, true).await.unwrap(); - alice_auto_submitted_hdr = "Auto-Submitted: auto-generated"; - } - _ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied", - }; - for t in [&alice, &bob] { - t.set_config_bool(Config::VerifiedOneOnOneChats, true) - .await - .unwrap(); - } - - assert_eq!( - Chatlist::try_load(&alice, 0, None, None) - .await - .unwrap() - .len(), - 0 - ); - assert_eq!( - Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), - 0 - ); - - // Step 1: Generate QR-code, ChatId(0) indicates setup-contact - let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); - // We want Bob to learn Alice's name from their messages, not from the QR code. - alice - .set_config(Config::Displayname, Some("Alice Exampleorg")) - .await - .unwrap(); - - // Step 2: Bob scans QR-code, sends vc-request - join_securejoin(&bob.ctx, &qr).await.unwrap(); - assert_eq!( - Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), - 1 - ); - let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let sent = bob.pop_sent_msg().await; - assert!(!sent.payload.contains("Bob Examplenet")); - assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap()); - let msg = alice.parse_msg(&sent).await; - assert!(!msg.was_encrypted()); - assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request"); - assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some()); - assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none()); - - // Step 3: Alice receives vc-request, sends vc-auth-required - alice.recv_msg_trash(&sent).await; - assert_eq!( - Chatlist::try_load(&alice, 0, None, None) - .await - .unwrap() - .len(), - 1 - ); - - let sent = alice.pop_sent_msg().await; - assert!(sent.payload.contains(alice_auto_submitted_hdr)); - assert!(!sent.payload.contains("Alice Exampleorg")); - let msg = bob.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vc-auth-required" - ); - let bob_chat = bob.create_chat(&alice).await; - assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false); - assert_eq!( - bob_chat.why_cant_send(&bob).await.unwrap(), - Some(CantSendReason::SecurejoinWait) - ); - if case == SetupContactCase::SecurejoinWaitTimeout { - SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT)); - assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); - } - - // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth - bob.recv_msg_trash(&sent).await; - let bob_chat = bob.create_chat(&alice).await; - assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); - - // Check Bob emitted the JoinerProgress event. - let event = bob - .evtracker - .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) - .await; - match event { - EventType::SecurejoinJoinerProgress { - contact_id, - progress, - } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - assert_eq!(contact_id, alice_contact_id); - assert_eq!(progress, 400); - } - _ => unreachable!(), - } - - // Check Bob sent the right message. - let sent = bob.pop_sent_msg().await; - assert!(sent.payload.contains("Auto-Submitted: auto-replied")); - assert!(!sent.payload.contains("Bob Examplenet")); - let mut msg = alice.parse_msg(&sent).await; - let vc_request_with_auth_ts_sent = msg - .get_header(HeaderDef::Date) - .and_then(|value| mailparse::dateparse(value).ok()) - .unwrap(); - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vc-request-with-auth" - ); - assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx) - .await - .unwrap() - .dc_fingerprint(); - assert_eq!( - *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), - bob_fp.hex() - ); - - if case == SetupContactCase::WrongAliceGossip { - let wrong_pubkey = load_self_public_key(&bob).await.unwrap(); - let alice_pubkey = msg - .gossiped_keys - .insert(alice_addr.to_string(), wrong_pubkey) - .unwrap(); - let contact_bob = alice.add_or_lookup_contact(&bob).await; - let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id) - .await - .unwrap(); - assert_eq!(handshake_msg, HandshakeMessage::Ignore); - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); - - msg.gossiped_keys - .insert(alice_addr.to_string(), alice_pubkey) - .unwrap(); - let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id) - .await - .unwrap(); - assert_eq!(handshake_msg, HandshakeMessage::Ignore); - assert!(contact_bob.is_verified(&alice.ctx).await.unwrap()); - return; - } - - // Alice should not yet have Bob verified - let contact_bob_id = - Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) - .await - .unwrap(); - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); - assert_eq!(contact_bob.get_authname(), ""); - - if case == SetupContactCase::CheckProtectionTimestamp { - SystemTime::shift(Duration::from_secs(3600)); - } - - // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm - alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) - .await - .unwrap(); - assert_eq!(contact_bob.get_authname(), "Bob Examplenet"); - assert!(contact_bob.get_name().is_empty()); - assert_eq!(contact_bob.is_bot(), false); - - // exactly one one-to-one chat should be visible for both now - // (check this before calling alice.create_chat() explicitly below) - assert_eq!( - Chatlist::try_load(&alice, 0, None, None) - .await - .unwrap() - .len(), - 1 - ); - assert_eq!( - Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), - 1 - ); - - // Check Alice got the verified message in her 1:1 chat. - { - let chat = alice.create_chat(&bob).await; - let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; - assert!(msg.is_info()); - let expected_text = chat_protection_enabled(&alice).await; - assert_eq!(msg.get_text(), expected_text); - if case == SetupContactCase::CheckProtectionTimestamp { - assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1); - } - } - - // Make sure Alice hasn't yet sent their name to Bob. - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id) - .await - .unwrap(); - match case { - SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"), - _ => assert_eq!(contact_alice.get_authname(), ""), - }; - - // Check Alice sent the right message to Bob. - let sent = alice.pop_sent_msg().await; - assert!(sent.payload.contains(alice_auto_submitted_hdr)); - assert!(!sent.payload.contains("Alice Exampleorg")); - let msg = bob.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vc-contact-confirm" - ); - - // Bob should not yet have Alice verified - assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false); - - // Step 7: Bob receives vc-contact-confirm - bob.recv_msg_trash(&sent).await; - assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true); - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id) - .await - .unwrap(); - assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"); - assert!(contact_alice.get_name().is_empty()); - assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot); - - if case != SetupContactCase::SecurejoinWaitTimeout { - // Later we check that the timeout message isn't added to the already protected chat. - SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1)); - assert_eq!( - bob_chat - .check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT) - .await - .unwrap(), - 0 - ); - } - - // Check Bob got expected info messages in his 1:1 chat. - let msg_cnt: usize = match case { - SetupContactCase::SecurejoinWaitTimeout => 3, - _ => 2, - }; - let mut i = 0..msg_cnt; - let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; - assert!(msg.is_info()); - assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await); - if case == SetupContactCase::SecurejoinWaitTimeout { - let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; - assert!(msg.is_info()); - assert_eq!( - msg.get_text(), - stock_str::securejoin_wait_timeout(&bob).await - ); - } - let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; - assert!(msg.is_info()); - assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_bad_qr() { - let bob = TestContext::new_bob().await; - let ret = join_securejoin(&bob.ctx, "not a qr code").await; - assert!(ret.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_bob_knows_alice() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // Ensure Bob knows Alice_FP - let alice_pubkey = load_self_public_key(&alice.ctx).await?; - let peerstate = Peerstate { - addr: "alice@example.org".into(), - last_seen: 10, - last_seen_autocrypt: 10, - prefer_encrypt: EncryptPreference::Mutual, - public_key: Some(alice_pubkey.clone()), - public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), - gossip_key: Some(alice_pubkey.clone()), - gossip_timestamp: 10, - gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), - verified_key: None, - verified_key_fingerprint: None, - verifier: None, - secondary_verified_key: None, - secondary_verified_key_fingerprint: None, - secondary_verifier: None, - backward_verified_key_id: None, - fingerprint_changed: false, - }; - peerstate.save_to_db(&bob.ctx.sql).await?; - - // Step 1: Generate QR-code, ChatId(0) indicates setup-contact - let qr = get_securejoin_qr(&alice.ctx, None).await?; - - // Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request - join_securejoin(&bob.ctx, &qr).await.unwrap(); - - // Check Bob emitted the JoinerProgress event. - let event = bob - .evtracker - .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) - .await; - match event { - EventType::SecurejoinJoinerProgress { - contact_id, - progress, - } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - assert_eq!(contact_id, alice_contact_id); - assert_eq!(progress, 400); - } - _ => unreachable!(), - } - - // Check Bob sent the right handshake message. - let sent = bob.pop_sent_msg().await; - let msg = alice.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vc-request-with-auth" - ); - assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); - assert_eq!( - *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), - bob_fp.hex() - ); - - // Alice should not yet have Bob verified - let (contact_bob_id, _modified) = Contact::add_or_lookup( - &alice.ctx, - "", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); - - // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm - alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); - - let sent = alice.pop_sent_msg().await; - let msg = bob.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vc-contact-confirm" - ); - - // Bob should not yet have Alice verified - let contact_alice_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; - assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false); - - // Step 7: Bob receives vc-contact-confirm - bob.recv_msg_trash(&sent).await; - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_setup_contact_concurrent_calls() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // do a scan that is not working as claire is never responding - let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012"; - let claire_id = join_securejoin(&bob, qr_stale).await?; - let chat = Chat::load_from_db(&bob, claire_id).await?; - assert!(!claire_id.is_special()); - assert_eq!(chat.typ, Chattype::Single); - assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de")); - - // subsequent scans shall abort existing ones or run concurrently - - // but they must not fail as otherwise the whole qr scanning becomes unusable until restart. - let qr = get_securejoin_qr(&alice, None).await?; - let alice_id = join_securejoin(&bob, &qr).await?; - let chat = Chat::load_from_db(&bob, alice_id).await?; - assert!(!alice_id.is_special()); - assert_eq!(chat.typ, Chattype::Single); - assert_ne!(claire_id, alice_id); - assert!(bob - .pop_sent_msg() - .await - .payload() - .contains("alice@example.org")); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_secure_join() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // We start with empty chatlists. - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0); - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - - let alice_chatid = - chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; - - // Step 1: Generate QR-code, secure-join implied by chatid - let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)) - .await - .unwrap(); - - // Step 2: Bob scans QR-code, sends vg-request - let bob_chatid = join_securejoin(&bob.ctx, &qr).await?; - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); - - let sent = bob.pop_sent_msg().await; - assert_eq!( - sent.recipient(), - EmailAddress::new("alice@example.org").unwrap() - ); - let msg = alice.parse_msg(&sent).await; - assert!(!msg.was_encrypted()); - assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request"); - assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some()); - assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none()); - - // Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`, - // but it was only used by Alice in `vg-request-with-auth`. - // New Delta Chat versions do not use `Secure-Join-Group` header at all - // and it is deprecated. - // Now `Secure-Join-Group` header - // is only sent in `vg-request-with-auth` for compatibility. - assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none()); - - // Step 3: Alice receives vg-request, sends vg-auth-required - alice.recv_msg_trash(&sent).await; - - let sent = alice.pop_sent_msg().await; - assert!(sent.payload.contains("Auto-Submitted: auto-replied")); - let msg = bob.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vg-auth-required" - ); - - // Step 4: Bob receives vg-auth-required, sends vg-request-with-auth - bob.recv_msg_trash(&sent).await; - let sent = bob.pop_sent_msg().await; - - // Check Bob emitted the JoinerProgress event. - let event = bob - .evtracker - .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) - .await; - match event { - EventType::SecurejoinJoinerProgress { - contact_id, - progress, - } => { - let alice_contact_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - assert_eq!(contact_id, alice_contact_id); - assert_eq!(progress, 400); - } - _ => unreachable!(), - } - - // Check Bob sent the right handshake message. - assert!(sent.payload.contains("Auto-Submitted: auto-replied")); - let msg = alice.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vg-request-with-auth" - ); - assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); - assert_eq!( - *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), - bob_fp.hex() - ); - - // Alice should not yet have Bob verified - let contact_bob_id = - Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await? - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); - - // Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added - alice.recv_msg_trash(&sent).await; - assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); - - let sent = alice.pop_sent_msg().await; - let msg = bob.parse_msg(&sent).await; - assert!(msg.was_encrypted()); - assert_eq!( - msg.get_header(HeaderDef::SecureJoin).unwrap(), - "vg-member-added" - ); - // Formally this message is auto-submitted, but as the member addition is a result of an - // explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would - // be strange to have it in "member-added" messages of verified groups only. - assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none()); - // This is a two-member group, but Alice must Autocrypt-gossip to her other devices. - assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some()); - - { - // Now Alice's chat with Bob should still be hidden, the verified message should - // appear in the group chat. - - let chat = alice.get_chat(&bob).await; - assert_eq!( - chat.blocked, - Blocked::Yes, - "Alice's 1:1 chat with Bob is not hidden" - ); - // There should be 3 messages in the chat: - // - The ChatProtectionEnabled message - // - You added member bob@example.net - let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await; - assert!(msg.is_info()); - let expected_text = chat_protection_enabled(&alice).await; - assert_eq!(msg.get_text(), expected_text); - } - - // Bob should not yet have Alice verified - let contact_alice_id = - Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; - assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false); - - // Step 7: Bob receives vg-member-added - bob.recv_msg(&sent).await; - { - // Bob has Alice verified, message shows up in the group chat. - assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); - let chat = bob.get_chat(&alice).await; - assert_eq!( - chat.blocked, - Blocked::Yes, - "Bob's 1:1 chat with Alice is not hidden" - ); - for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() { - if let chat::ChatItem::Message { msg_id } = item { - let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); - let text = msg.get_text(); - println!("msg {msg_id} text: {text}"); - } - } - } - - let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?; - assert!(bob_chat.is_protected()); - assert!(bob_chat.typ == Chattype::Group); - - // On this "happy path", Alice and Bob get only a group-chat where all information are added to. - // The one-to-one chats are used internally for the hidden handshake messages, - // however, should not be visible in the UIs. - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1); - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); - - // If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear. - let bobs_chat_with_alice = bob.create_chat(&alice).await; - let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await; - alice.recv_msg(&sent).await; - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2); - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_no_qr() -> Result<()> { - let alice = TestContext::new_alice().await; - - let mime = br#"Subject: First thread -Message-ID: first@example.org -To: Alice , Bob -From: Claire -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First thread."#; - - receive_imf(&alice, mime, false).await?; - let msg = alice.get_last_msg().await; - let chat_id = msg.chat_id; - - assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_unknown_sender() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - tcm.execute_securejoin(&alice, &bob).await; - - let alice_chat_id = alice - .create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob]) - .await; - - let sent = alice.send_text(alice_chat_id, "Hi!").await; - let bob_chat_id = bob.recv_msg(&sent).await.chat_id; - - let sent = bob.send_text(bob_chat_id, "Hi hi!").await; - - let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; - alice.pop_sent_msg().await; - - // The message from Bob is delivered late, Bob is already removed. - let msg = alice.recv_msg(&sent).await; - assert_eq!(msg.text, "Hi hi!"); - assert_eq!(msg.error.unwrap(), "Unknown sender for this chat."); - - Ok(()) - } - - /// Tests that Bob gets Alice as verified - /// if `vc-contact-confirm` is lost but Alice then sends - /// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_lost_contact_confirm() { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - for t in [&alice, &bob] { - t.set_config_bool(Config::VerifiedOneOnOneChats, true) - .await - .unwrap(); - } - - let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); - join_securejoin(&bob.ctx, &qr).await.unwrap(); - - // vc-request - let sent = bob.pop_sent_msg().await; - alice.recv_msg_trash(&sent).await; - - // vc-auth-required - let sent = alice.pop_sent_msg().await; - bob.recv_msg_trash(&sent).await; - - // vc-request-with-auth - let sent = bob.pop_sent_msg().await; - alice.recv_msg_trash(&sent).await; - - // Alice has Bob verified now. - let contact_bob_id = - Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) - .await - .unwrap(); - assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); - - // Alice sends vc-contact-confirm, but it gets lost. - let _sent_vc_contact_confirm = alice.pop_sent_msg().await; - - // Bob should not yet have Alice verified - let contact_alice_id = - Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown) - .await - .expect("Error looking up contact") - .expect("Contact not found"); - let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); - assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false); - - // Alice sends a text message to Bob. - let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await; - let chat_id = received_hello.chat_id; - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - assert_eq!(chat.is_protected(), true); - - // Received text message in a verified 1:1 chat results in backward verification - // and Bob now marks alice as verified. - let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); - assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); - } - - /// An unencrypted message with already known Autocrypt key, but sent from another address, - /// means that it's rather a new contact sharing the same key than the existing one changed its - /// address, otherwise it would already have our key to encrypt. - /// - /// This is a regression test for a bug where DC wrongly executed AEAP in this case. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_shared_bobs_key() -> 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(); - - tcm.execute_securejoin(bob, alice).await; - - let export_dir = tempfile::tempdir().unwrap(); - imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?; - let bob2 = &TestContext::new().await; - let bob2_addr = "bob2@example.net"; - bob2.configure_addr(bob2_addr).await; - imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; - - tcm.execute_securejoin(bob2, alice).await; - - let bob3 = &TestContext::new().await; - let bob3_addr = "bob3@example.net"; - bob3.configure_addr(bob3_addr).await; - imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; - tcm.send_recv(bob3, alice, "hi Alice!").await; - let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await; - assert!(msg.get_showpadlock()); - - let mut bob_ids = HashSet::new(); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown) - .await? - .unwrap(), - ); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown) - .await? - .unwrap(), - ); - bob_ids.insert( - Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown) - .await? - .unwrap(), - ); - assert_eq!(bob_ids.len(), 3); - Ok(()) - } -} +mod securejoin_tests; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 8782365185..60177808b5 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -1,31 +1,40 @@ -//! Bob's side of SecureJoin handling. -//! -//! This are some helper functions around [`BobState`] which augment the state changes with -//! the required user interactions. +//! Bob's side of SecureJoin handling, the joiner-side. use anyhow::{Context as _, Result}; -use super::bobstate::{BobHandshakeStage, BobState}; use super::qrinvite::QrInvite; use super::HandshakeMessage; -use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus}; +use crate::chat::{self, is_contact_in_chat, ChatId, ProtectionStatus}; use crate::constants::{self, Blocked, Chattype}; -use crate::contact::Contact; +use crate::contact::Origin; use crate::context::Context; use crate::events::EventType; +use crate::key::{load_self_public_key, DcKey}; +use crate::message::{Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; +use crate::param::Param; +use crate::securejoin::{encrypted_and_signed, verify_sender_by_fingerprint, ContactId}; +use crate::stock_str; use crate::sync::Sync::*; use crate::tools::{create_smeared_timestamp, time}; -use crate::{chat, stock_str}; /// Starts the securejoin protocol with the QR `invite`. /// -/// This will try to start the securejoin protocol for the given QR `invite`. If it -/// succeeded the protocol state will be tracked in `self`. +/// This will try to start the securejoin protocol for the given QR `invite`. +/// +/// If Bob already has Alice's key, he sends `AUTH` token +/// and forgets about the invite. +/// If Bob does not yet have Alice's key, he sends `vc-request` +/// or `vg-request` message and stores a row in the `bobstate` table +/// so he can check Alice's key against the fingerprint +/// and send `AUTH` token later. /// /// This function takes care of handling multiple concurrent joins and handling errors while /// starting the protocol. /// +/// # Bob - the joiner's side +/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 +/// /// # Returns /// /// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1 @@ -42,22 +51,47 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul .await .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?; - // Now start the protocol and initialise the state - let (state, stage, aborted_states) = - BobState::start_protocol(context, invite.clone(), chat_id).await?; - for state in aborted_states { - error!(context, "Aborting previously unfinished QR Join process."); - state.notify_aborted(context, "New QR code scanned").await?; - state.emit_progress(context, JoinerProgress::Error); - } - if matches!(stage, BobHandshakeStage::RequestWithAuthSent) { - state.emit_progress(context, JoinerProgress::RequestWithAuthSent); + ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?; + context.emit_event(EventType::ContactsChanged(None)); + + // Now start the protocol and initialise the state. + { + let peer_verified = + verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) + .await?; + + if peer_verified { + // The scanned fingerprint matches Alice's key, we can proceed to step 4b. + info!(context, "Taking securejoin protocol shortcut"); + send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) + .await?; + + // Mark 1:1 chat as verified already. + chat_id + .set_protection( + context, + ProtectionStatus::Protected, + time(), + Some(invite.contact_id()), + ) + .await?; + + context.emit_event(EventType::SecurejoinJoinerProgress { + contact_id: invite.contact_id(), + progress: JoinerProgress::RequestWithAuthSent.to_usize(), + }); + } else { + send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?; + + insert_new_db_entry(context, invite.clone(), chat_id).await?; + } } + match invite { QrInvite::Group { .. } => { // For a secure-join we need to create the group and add the contact. The group will // only become usable once the protocol is finished. - let group_chat_id = state.joining_chat_id(context).await?; + let group_chat_id = joining_chat_id(context, &invite, chat_id).await?; if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? { chat::add_to_chat_contacts_table( context, @@ -74,7 +108,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul QrInvite::Contact { .. } => { // For setup-contact the BobState already ensured the 1:1 chat exists because it // uses it to send the handshake messages. - let chat_id = state.alice_chat(); // Calculate the sort timestamp before checking the chat protection status so that if we // race with its change, we don't add our message below the protection message. let sort_to_bottom = true; @@ -93,6 +126,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul Some(ts_start), None, None, + None, ) .await?; chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT); @@ -102,6 +136,19 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul } } +/// Inserts a new entry in the bobstate table. +/// +/// Returns the ID of the newly inserted entry. +async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result { + context + .sql + .insert( + "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);", + (invite, 0, chat_id), + ) + .await +} + /// Handles `vc-auth-required` and `vg-auth-required` handshake messages. /// /// # Bob - the joiner's side @@ -110,121 +157,214 @@ pub(super) async fn handle_auth_required( context: &Context, message: &MimeMessage, ) -> Result { - let Some(mut bobstate) = BobState::from_db(&context.sql).await? else { - return Ok(HandshakeMessage::Ignore); - }; + // Load all Bob states that expect `vc-auth-required` or `vg-auth-required`. + let bob_states: Vec<(i64, QrInvite, ChatId)> = context + .sql + .query_map( + "SELECT id, invite, chat_id FROM bobstate", + (), + |row| { + let row_id: i64 = row.get(0)?; + let invite: QrInvite = row.get(1)?; + let chat_id: ChatId = row.get(2)?; + Ok((row_id, invite, chat_id)) + }, + |rows| rows.collect::, _>>().map_err(Into::into), + ) + .await?; - match bobstate.handle_auth_required(context, message).await? { - Some(BobHandshakeStage::Terminated(why)) => { - bobstate.notify_aborted(context, why).await?; - Ok(HandshakeMessage::Done) + info!( + context, + "Bob Step 4 - handling {{vc,vg}}-auth-required message." + ); + + let mut auth_sent = false; + for (bobstate_row_id, invite, chat_id) in bob_states { + if !encrypted_and_signed(context, message, invite.fingerprint()) { + continue; + } + + if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await? + { + continue; } - Some(_stage) => { - if bobstate.is_join_group() { + + info!(context, "Fingerprint verified.",); + send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?; + context + .sql + .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,)) + .await?; + + match invite { + QrInvite::Contact { .. } => {} + QrInvite::Group { .. } => { // The message reads "Alice replied, waiting to be added to the group…", // so only show it on secure-join and not on setup-contact. - let contact_id = bobstate.invite().contact_id(); + let contact_id = invite.contact_id(); let msg = stock_str::secure_join_replies(context, contact_id).await; - let chat_id = bobstate.joining_chat_id(context).await?; + let chat_id = joining_chat_id(context, &invite, chat_id).await?; chat::add_info_msg(context, chat_id, &msg, time()).await?; } - bobstate - .set_peer_verified(context, message.timestamp_sent) - .await?; - bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent); - Ok(HandshakeMessage::Done) } - None => Ok(HandshakeMessage::Ignore), - } -} -/// Private implementations for user interactions about this [`BobState`]. -impl BobState { - fn is_join_group(&self) -> bool { - match self.invite() { - QrInvite::Contact { .. } => false, - QrInvite::Group { .. } => true, - } - } + chat_id + .set_protection( + context, + ProtectionStatus::Protected, + message.timestamp_sent, + Some(invite.contact_id()), + ) + .await?; - pub(crate) fn emit_progress(&self, context: &Context, progress: JoinerProgress) { - let contact_id = self.invite().contact_id(); context.emit_event(EventType::SecurejoinJoinerProgress { - contact_id, - progress: progress.into(), + contact_id: invite.contact_id(), + progress: JoinerProgress::RequestWithAuthSent.to_usize(), }); + + auth_sent = true; } - /// Returns the [`ChatId`] of the chat being joined. - /// - /// This is the chat in which you want to notify the user as well. - /// - /// When joining a group this is the [`ChatId`] of the group chat, when verifying a - /// contact this is the [`ChatId`] of the 1:1 chat. The 1:1 chat is assumed to exist - /// because a [`BobState`] can not exist without, the group chat will be created if it - /// does not yet exist. - async fn joining_chat_id(&self, context: &Context) -> Result { - match self.invite() { - QrInvite::Contact { .. } => Ok(self.alice_chat()), - QrInvite::Group { - ref grpid, - ref name, - .. - } => { - let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? { - Some((chat_id, _protected, _blocked)) => { - chat_id.unblock_ex(context, Nosync).await?; - chat_id - } - None => { - ChatId::create_multiuser_record( - context, - Chattype::Group, - grpid, - name, - Blocked::Not, - ProtectionStatus::Unprotected, // protection is added later as needed - None, - create_smeared_timestamp(context), - ) - .await? - } - }; - Ok(group_chat_id) + if auth_sent { + // Delete the message from IMAP server. + Ok(HandshakeMessage::Done) + } else { + // We have not found any corresponding AUTH codes, + // maybe another Bob device has scanned the QR code. + // Leave the message on IMAP server and let the other device + // process it. + Ok(HandshakeMessage::Ignore) + } +} + +/// Sends the requested handshake message to Alice. +pub(crate) async fn send_handshake_message( + context: &Context, + invite: &QrInvite, + chat_id: ChatId, + step: BobHandshakeMsg, +) -> Result<()> { + let mut msg = Message { + viewtype: Viewtype::Text, + text: step.body_text(invite), + hidden: true, + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::SecurejoinMessage); + + // Sends the step in Secure-Join header. + msg.param.set(Param::Arg, step.securejoin_header(invite)); + + match step { + BobHandshakeMsg::Request => { + // Sends the Secure-Join-Invitenumber header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.invitenumber()); + msg.force_plaintext(); + } + BobHandshakeMsg::RequestWithAuth => { + // Sends the Secure-Join-Auth header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.authcode()); + msg.param.set_int(Param::GuaranteeE2ee, 1); + + // Sends our own fingerprint in the Secure-Join-Fingerprint header. + let bob_fp = load_self_public_key(context).await?.dc_fingerprint(); + msg.param.set(Param::Arg3, bob_fp.hex()); + + // Sends the grpid in the Secure-Join-Group header. + // + // `Secure-Join-Group` header is deprecated, + // but old Delta Chat core requires that Alice receives it. + // + // Previous Delta Chat core also sent `Secure-Join-Group` header + // in `vg-request` messages, + // but it was not used on the receiver. + if let QrInvite::Group { ref grpid, .. } = invite { + msg.param.set(Param::Arg4, grpid); } } + }; + + chat::send_msg(context, chat_id, &mut msg).await?; + Ok(()) +} + +/// Identifies the SecureJoin handshake messages Bob can send. +pub(crate) enum BobHandshakeMsg { + /// vc-request or vg-request + Request, + /// vc-request-with-auth or vg-request-with-auth + RequestWithAuth, +} + +impl BobHandshakeMsg { + /// Returns the text to send in the body of the handshake message. + /// + /// This text has no significance to the protocol, but would be visible if users see + /// this email message directly, e.g. when accessing their email without using + /// DeltaChat. + fn body_text(&self, invite: &QrInvite) -> String { + format!("Secure-Join: {}", self.securejoin_header(invite)) } - /// Notifies the user that the SecureJoin was aborted. + /// Returns the `Secure-Join` header value. /// - /// This creates an info message in the chat being joined. - async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> { - let contact = Contact::get_by_id(context, self.invite().contact_id()).await?; - let mut msg = stock_str::contact_not_verified(context, &contact).await; - msg += " ("; - msg += why; - msg += ")"; - let chat_id = self.joining_chat_id(context).await?; - chat::add_info_msg(context, chat_id, &msg, time()).await?; - warn!( - context, - "StockMessage::ContactNotVerified posted to joining chat ({})", why - ); - Ok(()) + /// This identifies the step this message is sending information about. Most protocol + /// steps include additional information into other headers, see + /// [`send_handshake_message`] for these. + fn securejoin_header(&self, invite: &QrInvite) -> &'static str { + match self { + Self::Request => match invite { + QrInvite::Contact { .. } => "vc-request", + QrInvite::Group { .. } => "vg-request", + }, + Self::RequestWithAuth => match invite { + QrInvite::Contact { .. } => "vc-request-with-auth", + QrInvite::Group { .. } => "vg-request-with-auth", + }, + } } +} - /// Turns 1:1 chat with SecureJoin peer into protected chat. - pub(crate) async fn set_peer_verified(&self, context: &Context, timestamp: i64) -> Result<()> { - let contact = Contact::get_by_id(context, self.invite().contact_id()).await?; - self.alice_chat() - .set_protection( - context, - ProtectionStatus::Protected, - timestamp, - Some(contact.id), - ) - .await?; - Ok(()) +/// Returns the [`ChatId`] of the chat being joined. +/// +/// This is the chat in which you want to notify the user as well. +/// +/// When joining a group this is the [`ChatId`] of the group chat, when verifying a +/// contact this is the [`ChatId`] of the 1:1 chat. +/// The group chat will be created if it does not yet exist. +async fn joining_chat_id( + context: &Context, + invite: &QrInvite, + alice_chat_id: ChatId, +) -> Result { + match invite { + QrInvite::Contact { .. } => Ok(alice_chat_id), + QrInvite::Group { + ref grpid, + ref name, + .. + } => { + let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? { + Some((chat_id, _protected, _blocked)) => { + chat_id.unblock_ex(context, Nosync).await?; + chat_id + } + None => { + ChatId::create_multiuser_record( + context, + Chattype::Group, + grpid, + name, + Blocked::Not, + ProtectionStatus::Unprotected, // protection is added later as needed + None, + create_smeared_timestamp(context), + ) + .await? + } + }; + Ok(group_chat_id) + } } } @@ -233,8 +373,6 @@ impl BobState { /// This has an `From for usize` impl yielding numbers between 0 and a 1000 /// which can be shown as a progress bar. pub(crate) enum JoinerProgress { - /// An error occurred. - Error, /// vg-vc-request-with-auth sent. /// /// Typically shows as "alice@addr verified, introducing myself." @@ -243,10 +381,10 @@ pub(crate) enum JoinerProgress { Succeeded, } -impl From for usize { - fn from(progress: JoinerProgress) -> Self { - match progress { - JoinerProgress::Error => 0, +impl JoinerProgress { + #[expect(clippy::wrong_self_convention)] + pub(crate) fn to_usize(self) -> usize { + match self { JoinerProgress::RequestWithAuthSent => 400, JoinerProgress::Succeeded => 1000, } diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs deleted file mode 100644 index dbc1573432..0000000000 --- a/src/securejoin/bobstate.rs +++ /dev/null @@ -1,517 +0,0 @@ -//! Secure-Join protocol state machine for Bob, the joiner-side. -//! -//! This module contains the state machine to run the Secure-Join handshake for Bob and does -//! not do any user interaction required by the protocol. Instead the state machine -//! provides all the information to its driver so it can perform the correct interactions. -//! -//! The [`BobState`] is only directly used to initially create it when starting the -//! protocol. - -use anyhow::Result; -use rusqlite::Connection; - -use super::qrinvite::QrInvite; -use super::{encrypted_and_signed, verify_sender_by_fingerprint}; -use crate::chat::{self, ChatId}; -use crate::config::Config; -use crate::contact::{ContactId, Origin}; -use crate::context::Context; -use crate::events::EventType; -use crate::headerdef::HeaderDef; -use crate::key::{load_self_public_key, DcKey}; -use crate::message::{Message, Viewtype}; -use crate::mimeparser::{MimeMessage, SystemMessage}; -use crate::param::Param; -use crate::securejoin::Peerstate; -use crate::sql::Sql; -use crate::tools::time; - -/// The stage of the [`BobState`] securejoin handshake protocol state machine. -/// -/// This does not concern itself with user interactions, only represents what happened to -/// the protocol state machine from handling this message. -#[derive(Clone, Copy, Debug, Display)] -pub enum BobHandshakeStage { - /// Step 2 completed: (vc|vg)-request message sent. - RequestSent, - /// Step 4 completed: (vc|vg)-request-with-auth message sent. - RequestWithAuthSent, - /// The protocol prematurely terminated with given reason. - Terminated(&'static str), -} - -/// The securejoin state kept while Bob is joining. -/// -/// This is stored in the database and loaded from there using [`BobState::from_db`]. To -/// create a new one use [`BobState::start_protocol`]. -/// -/// This purposefully has nothing optional, the state is always fully valid. However once a -/// terminal state is reached in [`BobState::next`] the entry in the database will already -/// have been deleted. -/// -/// # Conducting the securejoin handshake -/// -/// The methods on this struct allow you to interact with the state and thus conduct the -/// securejoin handshake for Bob. The methods only concern themselves with the protocol -/// state and explicitly avoid performing any user interactions required by securejoin. -/// This simplifies the concerns and logic required in both the callers and in the state -/// management. The return values can be used to understand what user interactions need to -/// happen. -/// -/// [`Bob`]: super::Bob -/// [`Bob::state`]: super::Bob::state -#[derive(Debug, Clone)] -pub struct BobState { - /// Database primary key. - id: i64, - /// The QR Invite code. - invite: QrInvite, - /// The next expected message from Alice. - next: SecureJoinStep, - /// The [`ChatId`] of the 1:1 chat with Alice, matching [`QrInvite::contact_id`]. - chat_id: ChatId, -} - -impl BobState { - /// Starts the securejoin protocol and creates a new [`BobState`]. - /// - /// The `chat_id` needs to be the ID of the 1:1 chat with Alice, this chat will be used - /// to exchange the SecureJoin handshake messages as well as for showing error messages. - /// - /// # Bob - the joiner's side - /// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 - /// - /// This currently aborts any other securejoin process if any did not yet complete. The - /// ChatIds of the relevant 1:1 chat of any aborted handshakes are returned so that you - /// can report the aboreted handshake in the chat. (Yes, there can only ever be one - /// ChatId in that Vec, the database doesn't care though.) - pub async fn start_protocol( - context: &Context, - invite: QrInvite, - chat_id: ChatId, - ) -> Result<(Self, BobHandshakeStage, Vec)> { - let peer_verified = - verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) - .await?; - - let (stage, next); - if peer_verified { - // The scanned fingerprint matches Alice's key, we can proceed to step 4b. - info!(context, "Taking securejoin protocol shortcut"); - send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) - .await?; - - stage = BobHandshakeStage::RequestWithAuthSent; - next = SecureJoinStep::ContactConfirm; - } else { - send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?; - - stage = BobHandshakeStage::RequestSent; - next = SecureJoinStep::AuthRequired; - }; - - let (id, aborted_states) = - Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?; - let state = Self { - id, - invite, - next, - chat_id, - }; - - if peer_verified { - // Mark 1:1 chat as verified already. - state.set_peer_verified(context, time()).await?; - } - - Ok((state, stage, aborted_states)) - } - - /// Inserts a new entry in the bobstate table, deleting all previous entries. - /// - /// Returns the ID of the newly inserted entry and all the aborted states. - async fn insert_new_db_entry( - context: &Context, - next: SecureJoinStep, - invite: QrInvite, - chat_id: ChatId, - ) -> Result<(i64, Vec)> { - context - .sql - .transaction(move |transaction| { - // We need to start a write transaction right away, so that we have the - // database locked and no one else can write to this table while we read the - // rows that we will delete. So start with a dummy UPDATE. - transaction.execute( - r#"UPDATE bobstate SET next_step=?;"#, - (SecureJoinStep::Terminated,), - )?; - let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?; - let mut aborted = Vec::new(); - for id in stmt.query_map((), |row| row.get::<_, i64>(0))? { - let id = id?; - let state = BobState::from_db_id(transaction, id)?; - aborted.push(state); - } - - // Finally delete everything and insert new row. - transaction.execute("DELETE FROM bobstate;", ())?; - transaction.execute( - "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);", - (invite, next, chat_id), - )?; - let id = transaction.last_insert_rowid(); - Ok((id, aborted)) - }) - .await - } - - /// Load [`BobState`] from the database. - pub async fn from_db(sql: &Sql) -> Result> { - // Because of how Self::start_protocol() updates the database we are currently - // guaranteed to only have one row. - sql.query_row_optional( - "SELECT id, invite, next_step, chat_id FROM bobstate;", - (), - |row| { - let s = BobState { - id: row.get(0)?, - invite: row.get(1)?, - next: row.get(2)?, - chat_id: row.get(3)?, - }; - Ok(s) - }, - ) - .await - } - - fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result { - connection.query_row( - "SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;", - (id,), - |row| { - let s = BobState { - id, - invite: row.get(0)?, - next: row.get(1)?, - chat_id: row.get(2)?, - }; - Ok(s) - }, - ) - } - - /// Returns the [`QrInvite`] used to create this [`BobState`]. - pub fn invite(&self) -> &QrInvite { - &self.invite - } - - /// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice). - pub fn alice_chat(&self) -> ChatId { - self.chat_id - } - - /// Updates the [`BobState::next`] field in memory and the database. - /// - /// If the next state is a terminal state it will remove this [`BobState`] from the - /// database. - /// - /// If a user scanned a new QR code after this [`BobState`] was loaded this update will - /// fail currently because starting a new joiner process currently kills any previously - /// running processes. This is a limitation which will go away in the future. - async fn update_next(&mut self, sql: &Sql, next: SecureJoinStep) -> Result<()> { - // TODO: write test verifying how this would fail. - match next { - SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => { - sql.execute( - "UPDATE bobstate SET next_step=? WHERE id=?;", - (next, self.id), - ) - .await?; - } - SecureJoinStep::Terminated | SecureJoinStep::Completed => { - sql.execute("DELETE FROM bobstate WHERE id=?;", (self.id,)) - .await?; - } - } - self.next = next; - Ok(()) - } - - /// Handles {vc,vg}-auth-required message of the securejoin handshake for Bob. - /// - /// If the message was not used for this handshake `None` is returned, otherwise the new - /// stage is returned. Once [`BobHandshakeStage::Terminated`] is reached this - /// [`BobState`] should be destroyed, - /// further calling it will just result in the messages being unused by this handshake. - pub(crate) async fn handle_auth_required( - &mut self, - context: &Context, - mime_message: &MimeMessage, - ) -> Result> { - let step = match mime_message.get_header(HeaderDef::SecureJoin) { - Some(step) => step, - None => { - warn!( - context, - "Message has no Secure-Join header: {}", - mime_message.get_rfc724_mid().unwrap_or_default() - ); - return Ok(None); - } - }; - if !self.is_msg_expected(context, step) { - info!(context, "{} message out of sync for BobState", step); - return Ok(None); - } - - info!( - context, - "Bob Step 4 - handling {{vc,vg}}-auth-required message." - ); - if !encrypted_and_signed(context, mime_message, self.invite.fingerprint()) { - let reason = if mime_message.was_encrypted() { - "Valid signature missing" - } else { - "Required encryption missing" - }; - self.update_next(&context.sql, SecureJoinStep::Terminated) - .await?; - return Ok(Some(BobHandshakeStage::Terminated(reason))); - } - if !verify_sender_by_fingerprint( - context, - self.invite.fingerprint(), - self.invite.contact_id(), - ) - .await? - { - self.update_next(&context.sql, SecureJoinStep::Terminated) - .await?; - return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); - } - info!(context, "Fingerprint verified.",); - - self.update_next(&context.sql, SecureJoinStep::ContactConfirm) - .await?; - self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth) - .await?; - Ok(Some(BobHandshakeStage::RequestWithAuthSent)) - } - - /// Returns `true` if the message is expected according to the protocol. - pub(crate) fn is_msg_expected(&self, context: &Context, step: &str) -> bool { - let variant_matches = match self.invite { - QrInvite::Contact { .. } => step.starts_with("vc-"), - QrInvite::Group { .. } => step.starts_with("vg-"), - }; - let step_matches = self.next.matches(context, step); - variant_matches && step_matches - } - - /// Handles a *vc-contact-confirm* or *vg-member-added* message. - /// - /// # Bob - the joiner's side - /// ## Step 7 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 - pub(crate) async fn step_contact_confirm(&mut self, context: &Context) -> Result<()> { - let fingerprint = self.invite.fingerprint(); - let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? - else { - return Ok(()); - }; - - // Mark peer as backward verified. - peerstate.backward_verified_key_id = - Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); - peerstate.save_to_db(&context.sql).await?; - - ContactId::scaleup_origin( - context, - &[self.invite.contact_id()], - Origin::SecurejoinJoined, - ) - .await?; - context.emit_event(EventType::ContactsChanged(None)); - - self.update_next(&context.sql, SecureJoinStep::Completed) - .await?; - Ok(()) - } - - /// Sends the requested handshake message to Alice. - /// - /// This takes care of adding the required headers for the step. - async fn send_handshake_message(&self, context: &Context, step: BobHandshakeMsg) -> Result<()> { - send_handshake_message(context, &self.invite, self.chat_id, step).await - } - - /// Returns whether we are waiting for a SecureJoin message from Alice, i.e. the protocol hasn't - /// yet completed. - pub(crate) fn in_progress(&self) -> bool { - !matches!( - self.next, - SecureJoinStep::Terminated | SecureJoinStep::Completed - ) - } -} - -/// Sends the requested handshake message to Alice. -/// -/// Same as [`BobState::send_handshake_message`] but this variation allows us to send this -/// message before we create the state in [`BobState::start_protocol`]. -async fn send_handshake_message( - context: &Context, - invite: &QrInvite, - chat_id: ChatId, - step: BobHandshakeMsg, -) -> Result<()> { - let mut msg = Message { - viewtype: Viewtype::Text, - text: step.body_text(invite), - hidden: true, - ..Default::default() - }; - msg.param.set_cmd(SystemMessage::SecurejoinMessage); - - // Sends the step in Secure-Join header. - msg.param.set(Param::Arg, step.securejoin_header(invite)); - - match step { - BobHandshakeMsg::Request => { - // Sends the Secure-Join-Invitenumber header in mimefactory.rs. - msg.param.set(Param::Arg2, invite.invitenumber()); - msg.force_plaintext(); - } - BobHandshakeMsg::RequestWithAuth => { - // Sends the Secure-Join-Auth header in mimefactory.rs. - msg.param.set(Param::Arg2, invite.authcode()); - msg.param.set_int(Param::GuaranteeE2ee, 1); - - // Sends our own fingerprint in the Secure-Join-Fingerprint header. - let bob_fp = load_self_public_key(context).await?.dc_fingerprint(); - msg.param.set(Param::Arg3, bob_fp.hex()); - - // Sends the grpid in the Secure-Join-Group header. - // - // `Secure-Join-Group` header is deprecated, - // but old Delta Chat core requires that Alice receives it. - // - // Previous Delta Chat core also sent `Secure-Join-Group` header - // in `vg-request` messages, - // but it was not used on the receiver. - if let QrInvite::Group { ref grpid, .. } = invite { - msg.param.set(Param::Arg4, grpid); - } - } - }; - - chat::send_msg(context, chat_id, &mut msg).await?; - Ok(()) -} - -/// Identifies the SecureJoin handshake messages Bob can send. -enum BobHandshakeMsg { - /// vc-request or vg-request - Request, - /// vc-request-with-auth or vg-request-with-auth - RequestWithAuth, -} - -impl BobHandshakeMsg { - /// Returns the text to send in the body of the handshake message. - /// - /// This text has no significance to the protocol, but would be visible if users see - /// this email message directly, e.g. when accessing their email without using - /// DeltaChat. - fn body_text(&self, invite: &QrInvite) -> String { - format!("Secure-Join: {}", self.securejoin_header(invite)) - } - - /// Returns the `Secure-Join` header value. - /// - /// This identifies the step this message is sending information about. Most protocol - /// steps include additional information into other headers, see - /// [`BobState::send_handshake_message`] for these. - fn securejoin_header(&self, invite: &QrInvite) -> &'static str { - match self { - Self::Request => match invite { - QrInvite::Contact { .. } => "vc-request", - QrInvite::Group { .. } => "vg-request", - }, - Self::RequestWithAuth => match invite { - QrInvite::Contact { .. } => "vc-request-with-auth", - QrInvite::Group { .. } => "vg-request-with-auth", - }, - } - } -} - -/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SecureJoinStep { - /// Expecting the auth-required message. - /// - /// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d. - AuthRequired, - /// Expecting the contact-confirm message. - /// - /// This corresponds to the `vc-contact-confirm` or `vg-member-added` message of step - /// 6b. - ContactConfirm, - /// The protocol terminated because of an error. - /// - /// The securejoin protocol terminated, this exists to ensure [`BobState`] can detect - /// when it earlier signalled that is should be terminated. It is an error to call with - /// this state. - Terminated, - /// The protocol completed. - /// - /// This exists to ensure [`BobState`] can detect when it earlier signalled that it is - /// complete. It is an error to call with this state. - Completed, -} - -impl SecureJoinStep { - /// Compares the legacy string representation of a step to a [`SecureJoinStep`] variant. - fn matches(&self, context: &Context, step: &str) -> bool { - match self { - Self::AuthRequired => step == "vc-auth-required" || step == "vg-auth-required", - Self::ContactConfirm => step == "vc-contact-confirm" || step == "vg-member-added", - SecureJoinStep::Terminated => { - warn!(context, "Terminated state for next securejoin step"); - false - } - SecureJoinStep::Completed => { - warn!(context, "Completed state for next securejoin step"); - false - } - } - } -} - -impl rusqlite::types::ToSql for SecureJoinStep { - fn to_sql(&self) -> rusqlite::Result> { - let num = match &self { - SecureJoinStep::AuthRequired => 0, - SecureJoinStep::ContactConfirm => 1, - SecureJoinStep::Terminated => 2, - SecureJoinStep::Completed => 3, - }; - let val = rusqlite::types::Value::Integer(num); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - -impl rusqlite::types::FromSql for SecureJoinStep { - fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { - i64::column_result(value).and_then(|val| match val { - 0 => Ok(SecureJoinStep::AuthRequired), - 1 => Ok(SecureJoinStep::ContactConfirm), - 2 => Ok(SecureJoinStep::Terminated), - 3 => Ok(SecureJoinStep::Completed), - _ => Err(rusqlite::types::FromSqlError::OutOfRange(val)), - }) - } -} diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs new file mode 100644 index 0000000000..75d0f007e2 --- /dev/null +++ b/src/securejoin/securejoin_tests.rs @@ -0,0 +1,984 @@ +use deltachat_contact_tools::{ContactAddress, EmailAddress}; + +use super::*; +use crate::chat::{remove_contact_from_chat, CantSendReason}; +use crate::chatlist::Chatlist; +use crate::constants::{self, Chattype}; +use crate::imex::{imex, ImexMode}; +use crate::receive_imf::receive_imf; +use crate::stock_str::{self, chat_protection_enabled}; +use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote}; +use crate::test_utils::{TestContext, TestContextManager}; +use crate::tools::SystemTime; +use std::collections::HashSet; +use std::time::Duration; + +#[derive(PartialEq)] +enum SetupContactCase { + Normal, + CheckProtectionTimestamp, + WrongAliceGossip, + SecurejoinWaitTimeout, + AliceIsBot, + AliceHasName, +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact() { + test_setup_contact_ex(SetupContactCase::Normal).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_protection_timestamp() { + test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_wrong_alice_gossip() { + test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_wait_timeout() { + test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_alice_is_bot() { + test_setup_contact_ex(SetupContactCase::AliceIsBot).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_alice_has_name() { + test_setup_contact_ex(SetupContactCase::AliceHasName).await +} + +async fn test_setup_contact_ex(case: SetupContactCase) { + let _n = TimeShiftFalsePositiveNote; + + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap(); + if case == SetupContactCase::AliceHasName { + alice + .set_config(Config::Displayname, Some("Alice")) + .await + .unwrap(); + } + let bob = tcm.bob().await; + bob.set_config(Config::Displayname, Some("Bob Examplenet")) + .await + .unwrap(); + let alice_auto_submitted_hdr; + match case { + SetupContactCase::AliceIsBot => { + alice.set_config_bool(Config::Bot, true).await.unwrap(); + alice_auto_submitted_hdr = "Auto-Submitted: auto-generated"; + } + _ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied", + }; + for t in [&alice, &bob] { + t.set_config_bool(Config::VerifiedOneOnOneChats, true) + .await + .unwrap(); + } + + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 0 + ); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 0 + ); + + // Step 1: Generate QR-code, ChatId(0) indicates setup-contact + let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); + // We want Bob to learn Alice's name from their messages, not from the QR code. + alice + .set_config(Config::Displayname, Some("Alice Exampleorg")) + .await + .unwrap(); + + // Step 2: Bob scans QR-code, sends vc-request + join_securejoin(&bob.ctx, &qr).await.unwrap(); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 1 + ); + let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let sent = bob.pop_sent_msg().await; + assert!(!sent.payload.contains("Bob Examplenet")); + assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap()); + let msg = alice.parse_msg(&sent).await; + assert!(!msg.was_encrypted()); + assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request"); + assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some()); + assert!(!msg.header_exists(HeaderDef::AutoSubmitted)); + + // Step 3: Alice receives vc-request, sends vc-auth-required + alice.recv_msg_trash(&sent).await; + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 0 + ); + + let sent = alice.pop_sent_msg().await; + assert!(sent.payload.contains(alice_auto_submitted_hdr)); + assert!(!sent.payload.contains("Alice Exampleorg")); + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-auth-required" + ); + let bob_chat = bob.get_chat(&alice).await; + assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false); + assert_eq!( + bob_chat.why_cant_send(&bob).await.unwrap(), + Some(CantSendReason::SecurejoinWait) + ); + if case == SetupContactCase::SecurejoinWaitTimeout { + SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT)); + assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); + } + + // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth + bob.recv_msg_trash(&sent).await; + let bob_chat = bob.get_chat(&alice).await; + assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true); + + // Check Bob emitted the JoinerProgress event. + let event = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) + .await; + match event { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => unreachable!(), + } + + // Check Bob sent the right message. + let sent = bob.pop_sent_msg().await; + assert!(sent.payload.contains("Auto-Submitted: auto-replied")); + assert!(!sent.payload.contains("Bob Examplenet")); + let mut msg = alice.parse_msg(&sent).await; + let vc_request_with_auth_ts_sent = msg + .get_header(HeaderDef::Date) + .and_then(|value| mailparse::dateparse(value).ok()) + .unwrap(); + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-request-with-auth" + ); + assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); + let bob_fp = load_self_public_key(&bob.ctx) + .await + .unwrap() + .dc_fingerprint(); + assert_eq!( + *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp.hex() + ); + + if case == SetupContactCase::WrongAliceGossip { + let wrong_pubkey = load_self_public_key(&bob).await.unwrap(); + let alice_pubkey = msg + .gossiped_keys + .insert(alice_addr.to_string(), wrong_pubkey) + .unwrap(); + let contact_bob = alice.add_or_lookup_email_contact(&bob).await; + let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id) + .await + .unwrap(); + assert_eq!(handshake_msg, HandshakeMessage::Ignore); + assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); + + msg.gossiped_keys + .insert(alice_addr.to_string(), alice_pubkey) + .unwrap(); + let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id) + .await + .unwrap(); + assert_eq!(handshake_msg, HandshakeMessage::Ignore); + assert!(contact_bob.is_verified(&alice.ctx).await.unwrap()); + return; + } + + // Alice should not yet have Bob verified + let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) + .await + .unwrap(); + assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false); + assert_eq!(contact_bob.get_authname(), ""); + + if case == SetupContactCase::CheckProtectionTimestamp { + SystemTime::shift(Duration::from_secs(3600)); + } + + // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm + alice.recv_msg_trash(&sent).await; + assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); + let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) + .await + .unwrap(); + assert_eq!(contact_bob.get_authname(), "Bob Examplenet"); + assert!(contact_bob.get_name().is_empty()); + assert_eq!(contact_bob.is_bot(), false); + + // exactly one one-to-one chat should be visible for both now + // (check this before calling alice.get_chat() explicitly below) + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 1 + ); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 1 + ); + + // Check Alice got the verified message in her 1:1 chat. + { + let chat = alice.get_chat(&bob).await; + let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await; + assert!(msg.is_info()); + let expected_text = chat_protection_enabled(&alice).await; + assert_eq!(msg.get_text(), expected_text); + if case == SetupContactCase::CheckProtectionTimestamp { + assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1); + } + } + + // Make sure Alice hasn't yet sent their name to Bob. + let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id) + .await + .unwrap(); + match case { + SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"), + _ => assert_eq!(contact_alice.get_authname(), ""), + }; + + // Check Alice sent the right message to Bob. + let sent = alice.pop_sent_msg().await; + assert!(sent.payload.contains(alice_auto_submitted_hdr)); + assert!(!sent.payload.contains("Alice Exampleorg")); + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm" + ); + + // Bob should not yet have Alice verified + assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false); + + // Step 7: Bob receives vc-contact-confirm + bob.recv_msg_trash(&sent).await; + assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true); + let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id) + .await + .unwrap(); + assert_eq!(contact_alice.get_authname(), "Alice Exampleorg"); + assert!(contact_alice.get_name().is_empty()); + assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot); + + if case != SetupContactCase::SecurejoinWaitTimeout { + // Later we check that the timeout message isn't added to the already protected chat. + SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1)); + assert_eq!( + bob_chat + .check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT) + .await + .unwrap(), + 0 + ); + } + + // Check Bob got expected info messages in his 1:1 chat. + let msg_cnt: usize = match case { + SetupContactCase::SecurejoinWaitTimeout => 3, + _ => 2, + }; + let mut i = 0..msg_cnt; + let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; + assert!(msg.is_info()); + assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await); + if case == SetupContactCase::SecurejoinWaitTimeout { + let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; + assert!(msg.is_info()); + assert_eq!( + msg.get_text(), + stock_str::securejoin_takes_longer(&bob).await + ); + } + let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await; + assert!(msg.is_info()); + assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_bad_qr() { + let bob = TestContext::new_bob().await; + let ret = join_securejoin(&bob.ctx, "not a qr code").await; + assert!(ret.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_bob_knows_alice() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // Ensure Bob knows Alice_FP + let alice_pubkey = load_self_public_key(&alice.ctx).await?; + let peerstate = Peerstate { + addr: "alice@example.org".into(), + last_seen: 10, + last_seen_autocrypt: 10, + prefer_encrypt: EncryptPreference::Mutual, + public_key: Some(alice_pubkey.clone()), + public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), + gossip_key: Some(alice_pubkey.clone()), + gossip_timestamp: 10, + gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()), + verified_key: None, + verified_key_fingerprint: None, + verifier: None, + secondary_verified_key: None, + secondary_verified_key_fingerprint: None, + secondary_verifier: None, + backward_verified_key_id: None, + fingerprint_changed: false, + }; + peerstate.save_to_db(&bob.ctx.sql).await?; + + // Step 1: Generate QR-code, ChatId(0) indicates setup-contact + let qr = get_securejoin_qr(&alice.ctx, None).await?; + + // Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request + join_securejoin(&bob.ctx, &qr).await.unwrap(); + + // Check Bob emitted the JoinerProgress event. + let event = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) + .await; + match event { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => unreachable!(), + } + + // Check Bob sent the right handshake message. + let sent = bob.pop_sent_msg().await; + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-request-with-auth" + ); + assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); + let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); + assert_eq!( + *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp.hex() + ); + + // Alice should not yet have Bob verified + let (contact_bob_id, _modified) = Contact::add_or_lookup( + &alice.ctx, + "", + &ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; + let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; + assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); + + // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm + alice.recv_msg_trash(&sent).await; + assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); + + let sent = alice.pop_sent_msg().await; + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm" + ); + + // Bob should not yet have Alice verified + let contact_alice_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; + assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false); + + // Step 7: Bob receives vc-contact-confirm + bob.recv_msg_trash(&sent).await; + assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_setup_contact_concurrent_calls() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // do a scan that is not working as claire is never responding + let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012"; + let claire_id = join_securejoin(&bob, qr_stale).await?; + let chat = Chat::load_from_db(&bob, claire_id).await?; + assert!(!claire_id.is_special()); + assert_eq!(chat.typ, Chattype::Single); + assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de")); + + // subsequent scans shall abort existing ones or run concurrently - + // but they must not fail as otherwise the whole qr scanning becomes unusable until restart. + let qr = get_securejoin_qr(&alice, None).await?; + let alice_id = join_securejoin(&bob, &qr).await?; + let chat = Chat::load_from_db(&bob, alice_id).await?; + assert!(!alice_id.is_special()); + assert_eq!(chat.typ, Chattype::Single); + assert_ne!(claire_id, alice_id); + assert!(bob + .pop_sent_msg() + .await + .payload() + .contains("alice@example.org")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_secure_join() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // We start with empty chatlists. + assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0); + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); + + let alice_chatid = + chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; + + // Step 1: Generate QR-code, secure-join implied by chatid + let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)) + .await + .unwrap(); + + // Step 2: Bob scans QR-code, sends vg-request + let bob_chatid = join_securejoin(&bob.ctx, &qr).await?; + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); + + let sent = bob.pop_sent_msg().await; + assert_eq!( + sent.recipient(), + EmailAddress::new("alice@example.org").unwrap() + ); + let msg = alice.parse_msg(&sent).await; + assert!(!msg.was_encrypted()); + assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request"); + assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some()); + assert!(!msg.header_exists(HeaderDef::AutoSubmitted)); + + // Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`, + // but it was only used by Alice in `vg-request-with-auth`. + // New Delta Chat versions do not use `Secure-Join-Group` header at all + // and it is deprecated. + // Now `Secure-Join-Group` header + // is only sent in `vg-request-with-auth` for compatibility. + assert!(!msg.header_exists(HeaderDef::SecureJoinGroup)); + + // Step 3: Alice receives vg-request, sends vg-auth-required + alice.recv_msg_trash(&sent).await; + + let sent = alice.pop_sent_msg().await; + assert!(sent.payload.contains("Auto-Submitted: auto-replied")); + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vg-auth-required" + ); + + // Step 4: Bob receives vg-auth-required, sends vg-request-with-auth + bob.recv_msg_trash(&sent).await; + let sent = bob.pop_sent_msg().await; + + // Check Bob emitted the JoinerProgress event. + let event = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. })) + .await; + match event { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => unreachable!(), + } + + // Check Bob sent the right handshake message. + assert!(sent.payload.contains("Auto-Submitted: auto-replied")); + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vg-request-with-auth" + ); + assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); + let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint(); + assert_eq!( + *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp.hex() + ); + + // Alice should not yet have Bob verified + let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) + .await? + .expect("Contact not found"); + let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?; + assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false); + + // Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added + alice.recv_msg_trash(&sent).await; + assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true); + + let sent = alice.pop_sent_msg().await; + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vg-member-added" + ); + // Formally this message is auto-submitted, but as the member addition is a result of an + // explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would + // be strange to have it in "member-added" messages of verified groups only. + assert!(!msg.header_exists(HeaderDef::AutoSubmitted)); + // This is a two-member group, but Alice must Autocrypt-gossip to her other devices. + assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some()); + + { + // Now Alice's chat with Bob should still be hidden, the verified message should + // appear in the group chat. + + let chat = alice.get_chat(&bob).await; + assert_eq!( + chat.blocked, + Blocked::Yes, + "Alice's 1:1 chat with Bob is not hidden" + ); + // There should be 3 messages in the chat: + // - The ChatProtectionEnabled message + // - You added member bob@example.net + let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await; + assert!(msg.is_info()); + let expected_text = chat_protection_enabled(&alice).await; + assert_eq!(msg.get_text(), expected_text); + } + + // Bob should not yet have Alice verified + let contact_alice_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?; + assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false); + + // Step 7: Bob receives vg-member-added + bob.recv_msg(&sent).await; + { + // Bob has Alice verified, message shows up in the group chat. + assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true); + let chat = bob.get_chat(&alice).await; + assert_eq!( + chat.blocked, + Blocked::Yes, + "Bob's 1:1 chat with Alice is not hidden" + ); + for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() { + if let chat::ChatItem::Message { msg_id } = item { + let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); + let text = msg.get_text(); + println!("msg {msg_id} text: {text}"); + } + } + } + + let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?; + assert!(bob_chat.is_protected()); + assert!(bob_chat.typ == Chattype::Group); + + // On this "happy path", Alice and Bob get only a group-chat where all information are added to. + // The one-to-one chats are used internally for the hidden handshake messages, + // however, should not be visible in the UIs. + assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1); + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); + + // If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear. + let bobs_chat_with_alice = bob.create_chat(&alice).await; + let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await; + alice.recv_msg(&sent).await; + assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2); + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_no_qr() -> Result<()> { + let alice = TestContext::new_alice().await; + + let mime = br#"Subject: First thread +Message-ID: first@example.org +To: Alice , Bob +From: Claire +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First thread."#; + + receive_imf(&alice, mime, false).await?; + let msg = alice.get_last_msg().await; + let chat_id = msg.chat_id; + + assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_unknown_sender() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + tcm.execute_securejoin(&alice, &bob).await; + + let alice_chat_id = alice + .create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob]) + .await; + + let sent = alice.send_text(alice_chat_id, "Hi!").await; + let bob_chat_id = bob.recv_msg(&sent).await.chat_id; + + let sent = bob.send_text(bob_chat_id, "Hi hi!").await; + + let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await; + remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; + alice.pop_sent_msg().await; + + // The message from Bob is delivered late, Bob is already removed. + let msg = alice.recv_msg(&sent).await; + assert_eq!(msg.text, "Hi hi!"); + assert_eq!(msg.error.unwrap(), "Unknown sender for this chat."); + + Ok(()) +} + +/// Tests that Bob gets Alice as verified +/// if `vc-contact-confirm` is lost but Alice then sends +/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_lost_contact_confirm() { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + for t in [&alice, &bob] { + t.set_config_bool(Config::VerifiedOneOnOneChats, true) + .await + .unwrap(); + } + + let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap(); + join_securejoin(&bob.ctx, &qr).await.unwrap(); + + // vc-request + let sent = bob.pop_sent_msg().await; + alice.recv_msg_trash(&sent).await; + + // vc-auth-required + let sent = alice.pop_sent_msg().await; + bob.recv_msg_trash(&sent).await; + + // vc-request-with-auth + let sent = bob.pop_sent_msg().await; + alice.recv_msg_trash(&sent).await; + + // Alice has Bob verified now. + let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id) + .await + .unwrap(); + assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true); + + // Alice sends vc-contact-confirm, but it gets lost. + let _sent_vc_contact_confirm = alice.pop_sent_msg().await; + + // Bob should not yet have Alice verified + let contact_alice_id = Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown) + .await + .expect("Error looking up contact") + .expect("Contact not found"); + let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); + assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false); + + // Alice sends a text message to Bob. + let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await; + let chat_id = received_hello.chat_id; + let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); + assert_eq!(chat.is_protected(), true); + + // Received text message in a verified 1:1 chat results in backward verification + // and Bob now marks alice as verified. + let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap(); + assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true); +} + +/// An unencrypted message with already known Autocrypt key, but sent from another address, +/// means that it's rather a new contact sharing the same key than the existing one changed its +/// address, otherwise it would already have our key to encrypt. +/// +/// This is a regression test for a bug where DC wrongly executed AEAP in this case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_shared_bobs_key() -> 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(); + + tcm.execute_securejoin(bob, alice).await; + + let export_dir = tempfile::tempdir().unwrap(); + imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?; + let bob2 = &TestContext::new().await; + let bob2_addr = "bob2@example.net"; + bob2.configure_addr(bob2_addr).await; + imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; + + tcm.execute_securejoin(bob2, alice).await; + + let bob3 = &TestContext::new().await; + let bob3_addr = "bob3@example.net"; + bob3.configure_addr(bob3_addr).await; + imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?; + let chat = bob3.create_email_chat(alice).await; + let sent = bob3.send_text(chat.id, "hi Alice!").await; + let msg = alice.recv_msg(&sent).await; + assert!(!msg.get_showpadlock()); + let chat = alice.create_email_chat(bob3).await; + let sent = alice.send_text(chat.id, "hi Bob3!").await; + let msg = bob3.recv_msg(&sent).await; + assert!(msg.get_showpadlock()); + + let mut bob_ids = HashSet::new(); + bob_ids.insert( + Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown) + .await? + .unwrap(), + ); + bob_ids.insert( + Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown) + .await? + .unwrap(), + ); + bob_ids.insert( + Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown) + .await? + .unwrap(), + ); + assert_eq!(bob_ids.len(), 3); + Ok(()) +} + +/// Tests Bob joining two groups by scanning two QR codes +/// from the same Alice at the same time. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parallel_securejoin() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat1_id = + chat::create_group_chat(alice, ProtectionStatus::Protected, "First chat").await?; + let alice_chat2_id = + chat::create_group_chat(alice, ProtectionStatus::Protected, "Second chat").await?; + + let qr1 = get_securejoin_qr(alice, Some(alice_chat1_id)).await?; + let qr2 = get_securejoin_qr(alice, Some(alice_chat2_id)).await?; + + // Bob scans both QR codes. + let bob_chat1_id = join_securejoin(bob, &qr1).await?; + let sent_vg_request1 = bob.pop_sent_msg().await; + + let bob_chat2_id = join_securejoin(bob, &qr2).await?; + let sent_vg_request2 = bob.pop_sent_msg().await; + + // Alice receives two `vg-request` messages + // and sends back two `vg-auth-required` messages. + alice.recv_msg_trash(&sent_vg_request1).await; + let sent_vg_auth_required1 = alice.pop_sent_msg().await; + + alice.recv_msg_trash(&sent_vg_request2).await; + let _sent_vg_auth_required2 = alice.pop_sent_msg().await; + + // Bob receives first `vg-auth-required` message. + // Bob has two securejoin processes started, + // so he should send two `vg-request-with-auth` messages. + bob.recv_msg_trash(&sent_vg_auth_required1).await; + + // Bob sends `vg-request-with-auth` messages. + let sent_vg_request_with_auth2 = bob.pop_sent_msg().await; + let sent_vg_request_with_auth1 = bob.pop_sent_msg().await; + + // Alice receives both `vg-request-with-auth` messages. + alice.recv_msg_trash(&sent_vg_request_with_auth1).await; + let sent_vg_member_added1 = alice.pop_sent_msg().await; + let joined_chat_id1 = bob.recv_msg(&sent_vg_member_added1).await.chat_id; + assert_eq!(joined_chat_id1, bob_chat1_id); + + alice.recv_msg_trash(&sent_vg_request_with_auth2).await; + let sent_vg_member_added2 = alice.pop_sent_msg().await; + let joined_chat_id2 = bob.recv_msg(&sent_vg_member_added2).await.chat_id; + assert_eq!(joined_chat_id2, bob_chat2_id); + + Ok(()) +} + +/// Tests Bob scanning setup contact QR codes of Alice and Fiona +/// concurrently. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parallel_setup_contact() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; + + // Bob scans Alice's QR code, + // but Alice is offline and takes a while to respond. + let alice_qr = get_securejoin_qr(alice, None).await?; + join_securejoin(bob, &alice_qr).await?; + let sent_alice_vc_request = bob.pop_sent_msg().await; + + // Bob scans Fiona's QR code while SecureJoin + // process with Alice is not finished. + let fiona_qr = get_securejoin_qr(fiona, None).await?; + join_securejoin(bob, &fiona_qr).await?; + let sent_fiona_vc_request = bob.pop_sent_msg().await; + + fiona.recv_msg_trash(&sent_fiona_vc_request).await; + let sent_fiona_vc_auth_required = fiona.pop_sent_msg().await; + + bob.recv_msg_trash(&sent_fiona_vc_auth_required).await; + let sent_fiona_vc_request_with_auth = bob.pop_sent_msg().await; + + fiona.recv_msg_trash(&sent_fiona_vc_request_with_auth).await; + let sent_fiona_vc_contact_confirm = fiona.pop_sent_msg().await; + + bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await; + let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await; + let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap(); + assert_eq!(bob_fiona_contact.is_verified(bob).await.unwrap(), true); + + // Alice gets online and previously started SecureJoin process finishes. + alice.recv_msg_trash(&sent_alice_vc_request).await; + let sent_alice_vc_auth_required = alice.pop_sent_msg().await; + + bob.recv_msg_trash(&sent_alice_vc_auth_required).await; + let sent_alice_vc_request_with_auth = bob.pop_sent_msg().await; + + alice.recv_msg_trash(&sent_alice_vc_request_with_auth).await; + let sent_alice_vc_contact_confirm = alice.pop_sent_msg().await; + + bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await; + let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await; + let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap(); + assert_eq!(bob_alice_contact.is_verified(bob).await.unwrap(), true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_wrong_auth_token() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + // Bob should already have Alice's key + // so that he can directly send vc-request-with-auth + tcm.send_recv(alice, bob, "hi").await; + + let alice_qr = get_securejoin_qr(alice, None).await?; + println!("{}", &alice_qr); + let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey="); + + join_securejoin(bob, &invalid_alice_qr).await?; + let sent = bob.pop_sent_msg().await; + + let msg = alice.parse_msg(&sent).await; + assert_eq!( + msg.get_header(HeaderDef::SecureJoin).unwrap(), + "vc-request-with-auth" + ); + + alice.recv_msg_trash(&sent).await; + + let alice_bob_contact = alice.add_or_lookup_contact(bob).await; + assert!(!alice_bob_contact.is_forward_verified(alice).await?); + + Ok(()) +} diff --git a/src/smtp.rs b/src/smtp.rs index 945196b7a1..89afc00b5c 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -90,13 +90,14 @@ impl Smtp { let lp = ConfiguredLoginParam::load(context) .await? .context("Not configured")?; + let proxy_config = ProxyConfig::load(context).await?; self.connect( context, &lp.smtp, &lp.smtp_password, - &lp.proxy_config, + &proxy_config, &lp.addr, - lp.strict_tls(), + lp.strict_tls(proxy_config.is_some()), lp.oauth2, ) .await @@ -438,6 +439,7 @@ pub(crate) async fn send_msg_to_smtp( None, None, None, + None, ) .await?; }; diff --git a/src/sql.rs b/src/sql.rs index efc68267b4..1dde1415c0 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -251,7 +251,7 @@ impl Sql { if recode_avatar { if let Some(avatar) = context.get_config(Config::Selfavatar).await? { - let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?; + let mut blob = BlobObject::from_path(context, Path::new(&avatar))?; match blob.recode_to_avatar_size(context).await { Ok(()) => { if let Some(path) = blob.to_abs_path().to_str() { @@ -458,7 +458,9 @@ impl Sql { /// in parallel with other transactions. NB: Creating and modifying temporary tables are also /// allowed with `query_only`, temporary tables aren't visible in other connections, but you /// need to pass `PRAGMA query_only=0;` to SQLite before that: - /// `pragma_update(None, "query_only", "0")`. + /// ```text + /// pragma_update(None, "query_only", "0") + /// ``` /// Also temporary tables need to be dropped because the connection is returned to the pool /// then. /// @@ -912,42 +914,51 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> { continue; } - if let Ok(stats) = tokio::fs::metadata(entry.path()).await { - if stats.is_dir() { - if let Err(e) = tokio::fs::remove_dir(entry.path()).await { - // The dir could be created not by a user, but by a desktop - // environment f.e. So, no warning. - info!( - context, - "Housekeeping: Cannot rmdir {}: {:#}.", - entry.path().display(), - e - ); - } + let stats = match tokio::fs::metadata(entry.path()).await { + Err(err) => { + warn!( + context, + "Cannot get metadata for {}: {:#}.", + entry.path().display(), + err + ); continue; } - unreferenced_count += 1; - let recently_created = - stats.created().is_ok_and(|t| t > keep_files_newer_than); - let recently_modified = - stats.modified().is_ok_and(|t| t > keep_files_newer_than); - let recently_accessed = - stats.accessed().is_ok_and(|t| t > keep_files_newer_than); - - if p == blobdir - && (recently_created || recently_modified || recently_accessed) - { + Ok(stats) => stats, + }; + + if stats.is_dir() { + if let Err(e) = tokio::fs::remove_dir(entry.path()).await { + // The dir could be created not by a user, but by a desktop + // environment f.e. So, no warning. info!( context, - "Housekeeping: Keeping new unreferenced file #{}: {:?}.", - unreferenced_count, - entry.file_name(), + "Housekeeping: Cannot rmdir {}: {:#}.", + entry.path().display(), + e ); - continue; } - } else { - unreferenced_count += 1; + continue; } + + unreferenced_count += 1; + let recently_created = stats.created().is_ok_and(|t| t > keep_files_newer_than); + let recently_modified = + stats.modified().is_ok_and(|t| t > keep_files_newer_than); + let recently_accessed = + stats.accessed().is_ok_and(|t| t > keep_files_newer_than); + + if p == blobdir && (recently_created || recently_modified || recently_accessed) + { + info!( + context, + "Housekeeping: Keeping new unreferenced file #{}: {:?}.", + unreferenced_count, + entry.file_name(), + ); + continue; + } + info!( context, "Housekeeping: Deleting unreferenced file #{}: {:?}.", @@ -1044,377 +1055,4 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> { } #[cfg(test)] -mod tests { - use super::*; - use crate::{test_utils::TestContext, EventType}; - - #[test] - fn test_maybe_add_file() { - let mut files = Default::default(); - maybe_add_file(&mut files, "$BLOBDIR/hello"); - maybe_add_file(&mut files, "$BLOBDIR/world.txt"); - maybe_add_file(&mut files, "world2.txt"); - maybe_add_file(&mut files, "$BLOBDIR"); - - assert!(files.contains("hello")); - assert!(files.contains("world.txt")); - assert!(!files.contains("world2.txt")); - assert!(!files.contains("$BLOBDIR")); - } - - #[test] - fn test_is_file_in_use() { - let mut files = Default::default(); - maybe_add_file(&mut files, "$BLOBDIR/hello"); - maybe_add_file(&mut files, "$BLOBDIR/world.txt"); - maybe_add_file(&mut files, "world2.txt"); - - assert!(is_file_in_use(&files, None, "hello")); - assert!(!is_file_in_use(&files, Some(".txt"), "hello")); - assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix")); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_table_exists() { - let t = TestContext::new().await; - assert!(t.ctx.sql.table_exists("msgs").await.unwrap()); - assert!(!t.ctx.sql.table_exists("foobar").await.unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_col_exists() { - let t = TestContext::new().await; - assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap()); - assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap()); - assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap()); - } - - /// Tests that auto_vacuum is enabled for new databases. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_auto_vacuum() -> Result<()> { - let t = TestContext::new().await; - - let query_only = true; - let auto_vacuum = t - .sql - .call(query_only, |conn| { - let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| { - let auto_vacuum: i32 = row.get(0)?; - Ok(auto_vacuum) - })?; - Ok(auto_vacuum) - }) - .await?; - - // auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL - assert_eq!(auto_vacuum, 2); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_housekeeping_db_closed() { - let t = TestContext::new().await; - - let avatar_src = t.dir.path().join("avatar.png"); - let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap(); - t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await - .unwrap(); - - let event_source = t.get_event_emitter(); - - let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); - assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); - - t.sql.close().await; - housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail - t.sql.open(&t, "".to_string()).await.unwrap(); - - let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); - assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); - - while let Ok(event) = event_source.try_recv() { - match event.typ { - EventType::Info(s) => assert!( - !s.contains("Keeping new unreferenced file"), - "File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)" - ), - EventType::Error(s) => panic!("{}", s), - _ => {} - } - } - } - - /// Regression test for a bug where housekeeping deleted drafts since their - /// `hidden` flag is set. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_housekeeping_dont_delete_drafts() { - let t = TestContext::new_alice().await; - - let chat = t.create_chat_with_contact("bob", "bob@example.com").await; - let mut new_draft = Message::new_text("This is my draft".to_string()); - chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap(); - - housekeeping(&t).await.unwrap(); - - let loaded_draft = chat.id.get_draft(&t).await.unwrap(); - assert_eq!(loaded_draft.unwrap().text, "This is my draft"); - } - - /// Tests that `housekeeping` deletes the blobs backup dir which is created normally by - /// `imex::import_backup`. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_housekeeping_delete_blobs_backup_dir() { - let t = TestContext::new_alice().await; - let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME); - tokio::fs::create_dir(&dir).await.unwrap(); - tokio::fs::write(dir.join("f"), "").await.unwrap(); - housekeeping(&t).await.unwrap(); - tokio::fs::create_dir(&dir).await.unwrap(); - } - - /// Regression test. - /// - /// Previously the code checking for existence of `config` table - /// checked it with `PRAGMA table_info("config")` but did not - /// drain `SqlitePool.fetch` result, only using the first row - /// returned. As a result, prepared statement for `PRAGMA` was not - /// finalized early enough, leaving reader connection in a broken - /// state after reopening the database, when `config` table - /// existed and `PRAGMA` returned non-empty result. - /// - /// Statements were not finalized due to a bug in sqlx: - /// - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_db_reopen() -> Result<()> { - use tempfile::tempdir; - - // The context is used only for logging. - let t = TestContext::new().await; - - // Create a separate empty database for testing. - let dir = tempdir()?; - let dbfile = dir.path().join("testdb.sqlite"); - let sql = Sql::new(dbfile); - - // Create database with all the tables. - sql.open(&t, "".to_string()).await.unwrap(); - sql.close().await; - - // Reopen the database - sql.open(&t, "".to_string()).await?; - sql.execute( - "INSERT INTO config (keyname, value) VALUES (?, ?);", - ("foo", "bar"), - ) - .await?; - - let value: Option = sql - .query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",)) - .await?; - assert_eq!(value.unwrap(), "bar"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_migration_flags() -> Result<()> { - let t = TestContext::new().await; - t.evtracker.get_info_contains("Opened database").await; - - // as migrations::run() was already executed on context creation, - // another call should not result in any action needed. - // this test catches some bugs where dbversion was forgotten to be persisted. - let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) = - migrations::run(&t, &t.sql).await?; - assert!(!recalc_fingerprints); - assert!(!update_icons); - assert!(!disable_server_delete); - assert!(!recode_avatar); - - info!(&t, "test_migration_flags: XXX END MARKER"); - - loop { - let evt = t - .evtracker - .get_matching(|evt| matches!(evt, EventType::Info(_))) - .await; - match evt { - EventType::Info(msg) => { - assert!( - !msg.contains("[migration]"), - "Migrations were run twice, you probably forgot to update the db version" - ); - if msg.contains("test_migration_flags: XXX END MARKER") { - break; - } - } - _ => unreachable!(), - } - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_check_passphrase() -> Result<()> { - use tempfile::tempdir; - - // The context is used only for logging. - let t = TestContext::new().await; - - // Create a separate empty database for testing. - let dir = tempdir()?; - let dbfile = dir.path().join("testdb.sqlite"); - let sql = Sql::new(dbfile.clone()); - - sql.check_passphrase("foo".to_string()).await?; - sql.open(&t, "foo".to_string()) - .await - .context("failed to open the database first time")?; - sql.close().await; - - // Reopen the database - let sql = Sql::new(dbfile); - - // Test that we can't open encrypted database without a passphrase. - assert!(sql.open(&t, "".to_string()).await.is_err()); - - // Now open the database with passpharse, it should succeed. - sql.check_passphrase("foo".to_string()).await?; - sql.open(&t, "foo".to_string()) - .await - .context("failed to open the database second time")?; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sql_change_passphrase() -> Result<()> { - use tempfile::tempdir; - - // The context is used only for logging. - let t = TestContext::new().await; - - // Create a separate empty database for testing. - let dir = tempdir()?; - let dbfile = dir.path().join("testdb.sqlite"); - let sql = Sql::new(dbfile.clone()); - - sql.open(&t, "foo".to_string()) - .await - .context("failed to open the database first time")?; - sql.close().await; - - // Change the passphrase from "foo" to "bar". - let sql = Sql::new(dbfile.clone()); - sql.open(&t, "foo".to_string()) - .await - .context("failed to open the database second time")?; - sql.change_passphrase("bar".to_string()) - .await - .context("failed to change passphrase")?; - - // Test that at least two connections are still working. - // This ensures that not only the connection which changed the password is working, - // but other connections as well. - { - let lock = sql.pool.read().await; - let pool = lock.as_ref().unwrap(); - let query_only = true; - let conn1 = pool.get(query_only).await?; - let conn2 = pool.get(query_only).await?; - conn1 - .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) - .unwrap(); - conn2 - .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) - .unwrap(); - } - - sql.close().await; - - let sql = Sql::new(dbfile); - - // Test that old passphrase is not working. - assert!(sql.open(&t, "foo".to_string()).await.is_err()); - - // Open the database with the new passphrase. - sql.check_passphrase("bar".to_string()).await?; - sql.open(&t, "bar".to_string()) - .await - .context("failed to open the database third time")?; - sql.close().await; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_query_only() -> Result<()> { - let t = TestContext::new().await; - - // `query_row` does not acquire write lock - // and operates on read-only connection. - // Using it to `INSERT` should fail. - let res = t - .sql - .query_row( - "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1", - ("xyz", "ijk"), - |row| { - let res: u32 = row.get(0)?; - Ok(res) - }, - ) - .await; - assert!(res.is_err()); - - // If you want to `INSERT` and get value via `RETURNING`, - // use `call_write` or `transaction`. - - let res: Result = t - .sql - .call_write(|conn| { - let val = conn.query_row( - "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2", - ("foo", "bar"), - |row| { - let res: u32 = row.get(0)?; - Ok(res) - }, - )?; - Ok(val) - }) - .await; - assert_eq!(res.unwrap(), 2); - - let res = t - .sql - .transaction(|t| { - let val = t.query_row( - "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3", - ("abc", "def"), - |row| { - let res: u32 = row.get(0)?; - Ok(res) - }, - )?; - Ok(val) - }) - .await; - assert_eq!(res.unwrap(), 3); - - Ok(()) - } - - /// Tests that incremental_vacuum does not fail. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_incremental_vacuum() -> Result<()> { - let t = TestContext::new().await; - - incremental_vacuum(&t).await?; - - Ok(()) - } -} +mod sql_tests; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 2532d68ad9..be093db42f 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -5,9 +5,11 @@ use deltachat_contact_tools::EmailAddress; use rusqlite::OptionalExtension; use crate::config::Config; +use crate::configure::EnteredLoginParam; use crate::constants::ShowEmails; use crate::context::Context; use crate::imap; +use crate::login_param::ConfiguredLoginParam; use crate::message::MsgId; use crate::provider::get_provider_by_domain; use crate::sql::Sql; @@ -1169,6 +1171,59 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid); .await?; } + inc_and_check(&mut migration_version, 130)?; + if dbversion < migration_version { + sql.execute_migration( + " +CREATE TABLE gossip_timestamp ( + chat_id INTEGER NOT NULL, + fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key. + timestamp INTEGER NOT NULL, + UNIQUE (chat_id, fingerprint) +) STRICT; +CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); +", + migration_version, + ) + .await?; + } + + inc_and_check(&mut migration_version, 131)?; + if dbversion < migration_version { + let entered_param = EnteredLoginParam::load(context).await?; + let configured_param = ConfiguredLoginParam::load_legacy(context).await?; + + sql.execute_migration_transaction( + |transaction| { + transaction.execute( + "CREATE TABLE transports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + addr TEXT NOT NULL, + entered_param TEXT NOT NULL, + configured_param TEXT NOT NULL, + UNIQUE(addr) + )", + (), + )?; + if let Some(configured_param) = configured_param { + transaction.execute( + "INSERT INTO transports (addr, entered_param, configured_param) + VALUES (?, ?, ?)", + ( + configured_param.addr.clone(), + serde_json::to_string(&entered_param)?, + configured_param.into_json()?, + ), + )?; + } + + Ok(()) + }, + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? @@ -1213,6 +1268,21 @@ impl Sql { } async fn execute_migration(&self, query: &str, version: i32) -> Result<()> { + self.execute_migration_transaction( + |transaction| { + transaction.execute_batch(query)?; + Ok(()) + }, + version, + ) + .await + } + + async fn execute_migration_transaction( + &self, + migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>, + version: i32, + ) -> Result<()> { self.transaction(move |transaction| { let curr_version: String = transaction.query_row( "SELECT IFNULL(value, ?) FROM config WHERE keyname=?;", @@ -1222,7 +1292,7 @@ impl Sql { let curr_version: i32 = curr_version.parse()?; ensure!(curr_version < version, "Db version must be increased"); Self::set_db_version_trans(transaction, version)?; - transaction.execute_batch(query)?; + migration(transaction)?; Ok(()) }) diff --git a/src/sql/sql_tests.rs b/src/sql/sql_tests.rs new file mode 100644 index 0000000000..265c9acf16 --- /dev/null +++ b/src/sql/sql_tests.rs @@ -0,0 +1,372 @@ +use super::*; +use crate::{test_utils::TestContext, EventType}; + +#[test] +fn test_maybe_add_file() { + let mut files = Default::default(); + maybe_add_file(&mut files, "$BLOBDIR/hello"); + maybe_add_file(&mut files, "$BLOBDIR/world.txt"); + maybe_add_file(&mut files, "world2.txt"); + maybe_add_file(&mut files, "$BLOBDIR"); + + assert!(files.contains("hello")); + assert!(files.contains("world.txt")); + assert!(!files.contains("world2.txt")); + assert!(!files.contains("$BLOBDIR")); +} + +#[test] +fn test_is_file_in_use() { + let mut files = Default::default(); + maybe_add_file(&mut files, "$BLOBDIR/hello"); + maybe_add_file(&mut files, "$BLOBDIR/world.txt"); + maybe_add_file(&mut files, "world2.txt"); + + assert!(is_file_in_use(&files, None, "hello")); + assert!(!is_file_in_use(&files, Some(".txt"), "hello")); + assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix")); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_table_exists() { + let t = TestContext::new().await; + assert!(t.ctx.sql.table_exists("msgs").await.unwrap()); + assert!(!t.ctx.sql.table_exists("foobar").await.unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_col_exists() { + let t = TestContext::new().await; + assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap()); + assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap()); + assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap()); +} + +/// Tests that auto_vacuum is enabled for new databases. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_auto_vacuum() -> Result<()> { + let t = TestContext::new().await; + + let query_only = true; + let auto_vacuum = t + .sql + .call(query_only, |conn| { + let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| { + let auto_vacuum: i32 = row.get(0)?; + Ok(auto_vacuum) + })?; + Ok(auto_vacuum) + }) + .await?; + + // auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL + assert_eq!(auto_vacuum, 2); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_housekeeping_db_closed() { + let t = TestContext::new().await; + + let avatar_src = t.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap(); + t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await + .unwrap(); + + let event_source = t.get_event_emitter(); + + let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); + + t.sql.close().await; + housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail + t.sql.open(&t, "".to_string()).await.unwrap(); + + let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); + assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]); + + while let Ok(event) = event_source.try_recv() { + match event.typ { + EventType::Info(s) => assert!( + !s.contains("Keeping new unreferenced file"), + "File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)" + ), + EventType::Error(s) => panic!("{}", s), + _ => {} + } + } +} + +/// Regression test for a bug where housekeeping deleted drafts since their +/// `hidden` flag is set. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_housekeeping_dont_delete_drafts() { + let t = TestContext::new_alice().await; + + let chat = t.create_chat_with_contact("bob", "bob@example.com").await; + let mut new_draft = Message::new_text("This is my draft".to_string()); + chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap(); + + housekeeping(&t).await.unwrap(); + + let loaded_draft = chat.id.get_draft(&t).await.unwrap(); + assert_eq!(loaded_draft.unwrap().text, "This is my draft"); +} + +/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by +/// `imex::import_backup`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_housekeeping_delete_blobs_backup_dir() { + let t = TestContext::new_alice().await; + let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME); + tokio::fs::create_dir(&dir).await.unwrap(); + tokio::fs::write(dir.join("f"), "").await.unwrap(); + housekeeping(&t).await.unwrap(); + tokio::fs::create_dir(&dir).await.unwrap(); +} + +/// Regression test. +/// +/// Previously the code checking for existence of `config` table +/// checked it with `PRAGMA table_info("config")` but did not +/// drain `SqlitePool.fetch` result, only using the first row +/// returned. As a result, prepared statement for `PRAGMA` was not +/// finalized early enough, leaving reader connection in a broken +/// state after reopening the database, when `config` table +/// existed and `PRAGMA` returned non-empty result. +/// +/// Statements were not finalized due to a bug in sqlx: +/// +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_db_reopen() -> Result<()> { + use tempfile::tempdir; + + // The context is used only for logging. + let t = TestContext::new().await; + + // Create a separate empty database for testing. + let dir = tempdir()?; + let dbfile = dir.path().join("testdb.sqlite"); + let sql = Sql::new(dbfile); + + // Create database with all the tables. + sql.open(&t, "".to_string()).await.unwrap(); + sql.close().await; + + // Reopen the database + sql.open(&t, "".to_string()).await?; + sql.execute( + "INSERT INTO config (keyname, value) VALUES (?, ?);", + ("foo", "bar"), + ) + .await?; + + let value: Option = sql + .query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",)) + .await?; + assert_eq!(value.unwrap(), "bar"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_migration_flags() -> Result<()> { + let t = TestContext::new().await; + t.evtracker.get_info_contains("Opened database").await; + + // as migrations::run() was already executed on context creation, + // another call should not result in any action needed. + // this test catches some bugs where dbversion was forgotten to be persisted. + let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) = + migrations::run(&t, &t.sql).await?; + assert!(!recalc_fingerprints); + assert!(!update_icons); + assert!(!disable_server_delete); + assert!(!recode_avatar); + + info!(&t, "test_migration_flags: XXX END MARKER"); + + loop { + let evt = t + .evtracker + .get_matching(|evt| matches!(evt, EventType::Info(_))) + .await; + match evt { + EventType::Info(msg) => { + assert!( + !msg.contains("[migration]"), + "Migrations were run twice, you probably forgot to update the db version" + ); + if msg.contains("test_migration_flags: XXX END MARKER") { + break; + } + } + _ => unreachable!(), + } + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_check_passphrase() -> Result<()> { + use tempfile::tempdir; + + // The context is used only for logging. + let t = TestContext::new().await; + + // Create a separate empty database for testing. + let dir = tempdir()?; + let dbfile = dir.path().join("testdb.sqlite"); + let sql = Sql::new(dbfile.clone()); + + sql.check_passphrase("foo".to_string()).await?; + sql.open(&t, "foo".to_string()) + .await + .context("failed to open the database first time")?; + sql.close().await; + + // Reopen the database + let sql = Sql::new(dbfile); + + // Test that we can't open encrypted database without a passphrase. + assert!(sql.open(&t, "".to_string()).await.is_err()); + + // Now open the database with passpharse, it should succeed. + sql.check_passphrase("foo".to_string()).await?; + sql.open(&t, "foo".to_string()) + .await + .context("failed to open the database second time")?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sql_change_passphrase() -> Result<()> { + use tempfile::tempdir; + + // The context is used only for logging. + let t = TestContext::new().await; + + // Create a separate empty database for testing. + let dir = tempdir()?; + let dbfile = dir.path().join("testdb.sqlite"); + let sql = Sql::new(dbfile.clone()); + + sql.open(&t, "foo".to_string()) + .await + .context("failed to open the database first time")?; + sql.close().await; + + // Change the passphrase from "foo" to "bar". + let sql = Sql::new(dbfile.clone()); + sql.open(&t, "foo".to_string()) + .await + .context("failed to open the database second time")?; + sql.change_passphrase("bar".to_string()) + .await + .context("failed to change passphrase")?; + + // Test that at least two connections are still working. + // This ensures that not only the connection which changed the password is working, + // but other connections as well. + { + let lock = sql.pool.read().await; + let pool = lock.as_ref().unwrap(); + let query_only = true; + let conn1 = pool.get(query_only).await?; + let conn2 = pool.get(query_only).await?; + conn1 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + conn2 + .query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(())) + .unwrap(); + } + + sql.close().await; + + let sql = Sql::new(dbfile); + + // Test that old passphrase is not working. + assert!(sql.open(&t, "foo".to_string()).await.is_err()); + + // Open the database with the new passphrase. + sql.check_passphrase("bar".to_string()).await?; + sql.open(&t, "bar".to_string()) + .await + .context("failed to open the database third time")?; + sql.close().await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_query_only() -> Result<()> { + let t = TestContext::new().await; + + // `query_row` does not acquire write lock + // and operates on read-only connection. + // Using it to `INSERT` should fail. + let res = t + .sql + .query_row( + "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1", + ("xyz", "ijk"), + |row| { + let res: u32 = row.get(0)?; + Ok(res) + }, + ) + .await; + assert!(res.is_err()); + + // If you want to `INSERT` and get value via `RETURNING`, + // use `call_write` or `transaction`. + + let res: Result = t + .sql + .call_write(|conn| { + let val = conn.query_row( + "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2", + ("foo", "bar"), + |row| { + let res: u32 = row.get(0)?; + Ok(res) + }, + )?; + Ok(val) + }) + .await; + assert_eq!(res.unwrap(), 2); + + let res = t + .sql + .transaction(|t| { + let val = t.query_row( + "INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3", + ("abc", "def"), + |row| { + let res: u32 = row.get(0)?; + Ok(res) + }, + )?; + Ok(val) + }) + .await; + assert_eq!(res.unwrap(), 3); + + Ok(()) +} + +/// Tests that incremental_vacuum does not fail. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_incremental_vacuum() -> Result<()> { + let t = TestContext::new().await; + + incremental_vacuum(&t).await?; + + Ok(()) +} diff --git a/src/sql/tables.sql b/src/sql/tables.sql index c52c298231..30317ead17 100644 --- a/src/sql/tables.sql +++ b/src/sql/tables.sql @@ -32,7 +32,7 @@ CREATE TABLE chats ( grpid TEXT DEFAULT '', param TEXT DEFAULT '', archived INTEGER DEFAULT 0, - gossiped_timestamp INTEGER DEFAULT 0, + gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossiped_timestamp table locations_send_begin INTEGER DEFAULT 0, locations_send_until INTEGER DEFAULT 0, locations_last_sent INTEGER DEFAULT 0, @@ -66,7 +66,7 @@ CREATE TABLE msgs ( msgrmsg INTEGER DEFAULT 1, bytes INTEGER DEFAULT 0, txt TEXT DEFAULT '', - txt_raw TEXT DEFAULT '', + txt_raw TEXT DEFAULT '', -- deprecated 2025-03-29 param TEXT DEFAULT '', starred INTEGER DEFAULT 0, timestamp_sent INTEGER DEFAULT 0, diff --git a/src/stock_str.rs b/src/stock_str.rs index d9e5afe46c..aca9821413 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -95,14 +95,6 @@ pub enum StockMessage { #[strum(props(fallback = "Archived chats"))] ArchivedChats = 40, - #[strum(props(fallback = "Autocrypt Setup Message"))] - AcSetupMsgSubject = 42, - - #[strum(props( - fallback = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\nTo decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device." - ))] - AcSetupMsgBody = 43, - #[strum(props( fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct." ))] @@ -438,9 +430,9 @@ pub enum StockMessage { SecurejoinWait = 190, #[strum(props( - fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message." + fallback = "The contact must be online to proceed.\n\nThis process will continue automatically in background." ))] - SecurejoinWaitTimeout = 191, + SecurejoinTakesLonger = 192, } impl StockMessage { @@ -537,14 +529,6 @@ trait StockStringMods: AsRef + Sized { } impl ContactId { - /// Get contact name and address for stock string, e.g. `Bob (bob@example.net)` - async fn get_stock_name_n_addr(self, context: &Context) -> String { - Contact::get_by_id(context, self) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_else(|_| self.to_string()) - } - /// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set. async fn get_stock_name(self, context: &Context) -> String { Contact::get_by_id(context, self) @@ -613,7 +597,7 @@ pub(crate) async fn msg_grp_name( .await .replace1(from_group) .replace2(to_group) - .replace3(&by_contact.get_stock_name_n_addr(context).await) + .replace3(&by_contact.get_stock_name(context).await) } } @@ -623,7 +607,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId } else { translated(context, StockMessage::MsgGrpImgChangedBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -637,7 +621,7 @@ pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) .await - .map(|contact| contact.get_authname_n_addr()) + .map(|contact| contact.get_authname_or_addr()) .unwrap_or_else(|_| addr.to_string()), _ => addr.to_string(), }; @@ -659,7 +643,7 @@ pub(crate) async fn msg_add_member_local( let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) .await - .map(|contact| contact.get_name_n_addr()) + .map(|contact| contact.get_display_name().to_string()) .unwrap_or_else(|_| addr.to_string()), _ => addr.to_string(), }; @@ -675,7 +659,7 @@ pub(crate) async fn msg_add_member_local( translated(context, StockMessage::MsgAddMemberBy) .await .replace1(whom) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -688,7 +672,7 @@ pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) .await - .map(|contact| contact.get_authname_n_addr()) + .map(|contact| contact.get_authname_or_addr()) .unwrap_or_else(|_| addr.to_string()), _ => addr.to_string(), }; @@ -710,7 +694,7 @@ pub(crate) async fn msg_del_member_local( let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) .await - .map(|contact| contact.get_name_n_addr()) + .map(|contact| contact.get_display_name().to_string()) .unwrap_or_else(|_| addr.to_string()), _ => addr.to_string(), }; @@ -726,7 +710,7 @@ pub(crate) async fn msg_del_member_local( translated(context, StockMessage::MsgDelMemberBy) .await .replace1(whom) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -742,7 +726,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI } else { translated(context, StockMessage::MsgGroupLeftBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -804,7 +788,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId } else { translated(context, StockMessage::MsgGrpImgDeletedBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -821,7 +805,7 @@ pub(crate) async fn secure_join_started( if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await { translated(context, StockMessage::SecureJoinStarted) .await - .replace1(&contact.get_name_n_addr()) + .replace1(contact.get_display_name()) .replace2(contact.get_display_name()) } else { format!("secure_join_started: unknown contact {inviter_contact_id}") @@ -840,9 +824,9 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String { translated(context, StockMessage::SecurejoinWait).await } -/// Stock string: `Could not yet establish guaranteed end-to-end encryption, but you may already send a message.`. -pub(crate) async fn securejoin_wait_timeout(context: &Context) -> String { - translated(context, StockMessage::SecurejoinWaitTimeout).await +/// Stock string: `The contact must be online to proceed. This process will continue automatically in background.`. +pub(crate) async fn securejoin_takes_longer(context: &Context) -> String { + translated(context, StockMessage::SecurejoinTakesLonger).await } /// Stock string: `Scan to chat with %1$s`. @@ -871,7 +855,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C /// Stock string: `%1$s verified.`. #[allow(dead_code)] pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String { - let addr = &contact.get_name_n_addr(); + let addr = contact.get_display_name(); translated(context, StockMessage::ContactVerified) .await .replace1(addr) @@ -879,7 +863,7 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St /// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`. pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String { - let addr = &contact.get_name_n_addr(); + let addr = contact.get_display_name(); translated(context, StockMessage::ContactNotVerified) .await .replace1(addr) @@ -897,16 +881,6 @@ pub(crate) async fn archived_chats(context: &Context) -> String { translated(context, StockMessage::ArchivedChats).await } -/// Stock string: `Autocrypt Setup Message`. -pub(crate) async fn ac_setup_msg_subject(context: &Context) -> String { - translated(context, StockMessage::AcSetupMsgSubject).await -} - -/// Stock string: `This is the Autocrypt Setup Message used to transfer...`. -pub(crate) async fn ac_setup_msg_body(context: &Context) -> String { - translated(context, StockMessage::AcSetupMsgBody).await -} - /// Stock string: `Multi Device Synchronization`. pub(crate) async fn sync_msg_subject(context: &Context) -> String { translated(context, StockMessage::SyncMsgSubject).await @@ -936,7 +910,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI } else { translated(context, StockMessage::MsgLocationEnabledBy) .await - .replace1(&contact.get_stock_name_n_addr(context).await) + .replace1(&contact.get_stock_name(context).await) } } @@ -998,7 +972,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled( } else { translated(context, StockMessage::MsgEphemeralTimerDisabledBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -1016,7 +990,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled( translated(context, StockMessage::MsgEphemeralTimerEnabledBy) .await .replace1(timer) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -1027,7 +1001,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co } else { translated(context, StockMessage::MsgEphemeralTimerMinuteBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -1038,7 +1012,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont } else { translated(context, StockMessage::MsgEphemeralTimerHourBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -1049,7 +1023,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta } else { translated(context, StockMessage::MsgEphemeralTimerDayBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -1060,7 +1034,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont } else { translated(context, StockMessage::MsgEphemeralTimerWeekBy) .await - .replace1(&by_contact.get_stock_name_n_addr(context).await) + .replace1(&by_contact.get_stock_name(context).await) } } @@ -1142,7 +1116,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes( translated(context, StockMessage::MsgEphemeralTimerMinutesBy) .await .replace1(minutes) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -1160,7 +1134,7 @@ pub(crate) async fn msg_ephemeral_timer_hours( translated(context, StockMessage::MsgEphemeralTimerHoursBy) .await .replace1(hours) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -1178,7 +1152,7 @@ pub(crate) async fn msg_ephemeral_timer_days( translated(context, StockMessage::MsgEphemeralTimerDaysBy) .await .replace1(days) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -1196,7 +1170,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks( translated(context, StockMessage::MsgEphemeralTimerWeeksBy) .await .replace1(weeks) - .replace2(&by_contact.get_stock_name_n_addr(context).await) + .replace2(&by_contact.get_stock_name(context).await) } } @@ -1348,20 +1322,19 @@ pub(crate) async fn new_group_send_first_message(context: &Context) -> String { /// Text to put in the [`Qr::Backup2`] rendered SVG image. /// -/// The default is "Scan to set up second device for ". The -/// account name and address are looked up from the context. +/// The default is "Scan to set up second device for NAME". +/// The account name (or address as fallback) are looked up from the context. /// /// [`Qr::Backup2`]: crate::qr::Qr::Backup2 pub(crate) async fn backup_transfer_qr(context: &Context) -> Result { - let contact = Contact::get_by_id(context, ContactId::SELF).await?; - let addr = contact.get_addr(); - let full_name = match context.get_config(Config::Displayname).await? { - Some(name) if name != addr => format!("{name} ({addr})"), - _ => addr.to_string(), + let name = if let Some(name) = context.get_config(Config::Displayname).await? { + name + } else { + context.get_primary_self_addr().await? }; Ok(translated(context, StockMessage::BackupTransferQr) .await - .replace1(&full_name)) + .replace1(&name)) } pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String { @@ -1439,187 +1412,4 @@ impl Accounts { } #[cfg(test)] -mod tests { - use num_traits::ToPrimitive; - - use super::*; - use crate::chat::delete_and_reset_all_device_msgs; - use crate::chatlist::Chatlist; - use crate::test_utils::TestContext; - - #[test] - fn test_enum_mapping() { - assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1); - assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2); - } - - #[test] - fn test_fallback() { - assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_stock_translation() { - let t = TestContext::new().await; - t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) - .await - .unwrap(); - assert_eq!(no_messages(&t).await, "xyz") - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_stock_translation_wrong_replacements() { - let t = TestContext::new().await; - assert!(t - .ctx - .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) - .await - .is_err()); - assert!(t - .ctx - .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) - .await - .is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_str() { - let t = TestContext::new().await; - assert_eq!(no_messages(&t).await, "No messages."); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_string_repl_str() { - let t = TestContext::new().await; - let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org") - .await - .unwrap(); - let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap(); - // uses %1$s substitution - assert_eq!( - contact_verified(&t, &contact).await, - "Someone (someone@example.org) verified." - ); - // We have no string using %1$d to test... - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_system_msg_simple() { - let t = TestContext::new().await; - assert_eq!( - msg_location_enabled(&t).await, - "Location streaming enabled." - ) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_system_msg_add_member_by_me() { - let t = TestContext::new().await; - assert_eq!( - msg_add_member_remote(&t, "alice@example.org").await, - "I added member alice@example.org." - ); - assert_eq!( - msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, - "You added member alice@example.org." - ) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_system_msg_add_member_by_me_with_displayname() { - let t = TestContext::new().await; - Contact::create(&t, "Alice", "alice@example.org") - .await - .expect("failed to create contact"); - assert_eq!( - msg_add_member_remote(&t, "alice@example.org").await, - "I added member alice@example.org." - ); - assert_eq!( - msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, - "You added member Alice (alice@example.org)." - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_stock_system_msg_add_member_by_other_with_displayname() { - let t = TestContext::new().await; - let contact_id = { - Contact::create(&t, "Alice", "alice@example.org") - .await - .expect("Failed to create contact Alice"); - Contact::create(&t, "Bob", "bob@example.com") - .await - .expect("failed to create bob") - }; - assert_eq!( - msg_add_member_local(&t, "alice@example.org", contact_id,).await, - "Member Alice (alice@example.org) added by Bob (bob@example.com)." - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_quota_exceeding_stock_str() -> Result<()> { - let t = TestContext::new().await; - let str = quota_exceeding(&t, 81).await; - assert!(str.contains("81% ")); - assert!(str.contains("100% ")); - assert!(!str.contains("%%")); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_partial_download_msg_body() -> Result<()> { - let t = TestContext::new().await; - let str = partial_download_msg_body(&t, 1024 * 1024).await; - assert_eq!(str, "1 MiB message"); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_update_device_chats() { - let t = TestContext::new().await; - t.update_device_chats().await.ok(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - - let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap()) - .await - .unwrap(); - let (self_talk_id, device_chat_id) = if chat0.is_self_talk() { - (chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap()) - } else { - (chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap()) - }; - - // delete self-talk first; this adds a message to device-chat about how self-talk can be restored - let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(); - self_talk_id.delete(&t).await.ok(); - assert_eq!( - chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(), - device_chat_msgs_before + 1 - ); - - // delete device chat - device_chat_id.delete(&t).await.ok(); - - // check, that the chatlist is empty - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // a subsequent call to update_device_chats() must not re-add manually deleted messages or chats - t.update_device_chats().await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // Reset all device messages. This normally happens due to account export and import. - // Check that update_device_chats() does not add welcome message for imported account. - delete_and_reset_all_device_msgs(&t).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - t.update_device_chats().await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } -} +mod stock_str_tests; diff --git a/src/stock_str/stock_str_tests.rs b/src/stock_str/stock_str_tests.rs new file mode 100644 index 0000000000..9b3fa018c5 --- /dev/null +++ b/src/stock_str/stock_str_tests.rs @@ -0,0 +1,179 @@ +use num_traits::ToPrimitive; + +use super::*; +use crate::chat::delete_and_reset_all_device_msgs; +use crate::chatlist::Chatlist; +use crate::test_utils::TestContext; + +#[test] +fn test_enum_mapping() { + assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1); + assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2); +} + +#[test] +fn test_fallback() { + assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_stock_translation() { + let t = TestContext::new().await; + t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) + .await + .unwrap(); + assert_eq!(no_messages(&t).await, "xyz") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_stock_translation_wrong_replacements() { + let t = TestContext::new().await; + assert!(t + .ctx + .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) + .await + .is_err()); + assert!(t + .ctx + .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) + .await + .is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_str() { + let t = TestContext::new().await; + assert_eq!(no_messages(&t).await, "No messages."); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_string_repl_str() { + let t = TestContext::new().await; + let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org") + .await + .unwrap(); + let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap(); + // uses %1$s substitution + assert_eq!(contact_verified(&t, &contact).await, "Someone verified."); + // We have no string using %1$d to test... +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_system_msg_simple() { + let t = TestContext::new().await; + assert_eq!( + msg_location_enabled(&t).await, + "Location streaming enabled." + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_system_msg_add_member_by_me() { + let t = TestContext::new().await; + assert_eq!( + msg_add_member_remote(&t, "alice@example.org").await, + "I added member alice@example.org." + ); + assert_eq!( + msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, + "You added member alice@example.org." + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_system_msg_add_member_by_me_with_displayname() { + let t = TestContext::new().await; + Contact::create(&t, "Alice", "alice@example.org") + .await + .expect("failed to create contact"); + assert_eq!( + msg_add_member_remote(&t, "alice@example.org").await, + "I added member alice@example.org." + ); + assert_eq!( + msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, + "You added member Alice." + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_stock_system_msg_add_member_by_other_with_displayname() { + let t = TestContext::new().await; + let contact_id = { + Contact::create(&t, "Alice", "alice@example.org") + .await + .expect("Failed to create contact Alice"); + Contact::create(&t, "Bob", "bob@example.com") + .await + .expect("failed to create bob") + }; + assert_eq!( + msg_add_member_local(&t, "alice@example.org", contact_id,).await, + "Member Alice added by Bob." + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_quota_exceeding_stock_str() -> Result<()> { + let t = TestContext::new().await; + let str = quota_exceeding(&t, 81).await; + assert!(str.contains("81% ")); + assert!(str.contains("100% ")); + assert!(!str.contains("%%")); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_partial_download_msg_body() -> Result<()> { + let t = TestContext::new().await; + let str = partial_download_msg_body(&t, 1024 * 1024).await; + assert_eq!(str, "1 MiB message"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_update_device_chats() { + let t = TestContext::new().await; + t.update_device_chats().await.ok(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + + let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap()) + .await + .unwrap(); + let (self_talk_id, device_chat_id) = if chat0.is_self_talk() { + (chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap()) + } else { + (chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap()) + }; + + // delete self-talk first; this adds a message to device-chat about how self-talk can be restored + let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(); + self_talk_id.delete(&t).await.ok(); + assert_eq!( + chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(), + device_chat_msgs_before + 1 + ); + + // delete device chat + device_chat_id.delete(&t).await.ok(); + + // check, that the chatlist is empty + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // a subsequent call to update_device_chats() must not re-add manually deleted messages or chats + t.update_device_chats().await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // Reset all device messages. This normally happens due to account export and import. + // Check that update_device_chats() does not add welcome message for imported account. + delete_and_reset_all_device_msgs(&t).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + t.update_device_chats().await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); +} diff --git a/src/summary.rs b/src/summary.rs index d2f55f3053..027348e16d 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -97,24 +97,25 @@ impl Summary { let prefix = if msg.state == MessageState::OutDraft { Some(SummaryPrefix::Draft(stock_str::draft(context).await)) } else if msg.from_id == ContactId::SELF { - if msg.is_info() || chat.is_self_talk() { + if msg.is_info() { None } else { Some(SummaryPrefix::Me(stock_str::self_msg(context).await)) } - } else { - match chat.typ { - Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => { - if msg.is_info() || contact.is_none() { - None - } else { - msg.get_override_sender_name() - .or_else(|| contact.map(|contact| msg.get_sender_name(contact))) - .map(SummaryPrefix::Username) - } - } - Chattype::Single => None, + } else if chat.typ == Chattype::Group + || chat.typ == Chattype::Broadcast + || chat.typ == Chattype::Mailinglist + || chat.is_self_talk() + { + if msg.is_info() || contact.is_none() { + None + } else { + msg.get_override_sender_name() + .or_else(|| contact.map(|contact| msg.get_sender_name(contact))) + .map(SummaryPrefix::Username) } + } else { + None }; let mut text = msg.get_summary_text(context).await; @@ -203,17 +204,10 @@ impl Message { append_text = true } Viewtype::File => { - if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage { - emoji = None; - type_name = Some(stock_str::ac_setup_msg_subject(context).await); - type_file = None; - append_text = false; - } else { - emoji = Some("📎"); - type_name = Some(stock_str::file(context).await); - type_file = self.get_filename(); - append_text = true - } + emoji = Some("📎"); + type_name = Some(stock_str::file(context).await); + type_file = self.get_filename(); + append_text = true } Viewtype::VideochatInvitation => { emoji = None; @@ -468,9 +462,10 @@ mod tests { ); // skipping prefix used for reactions summaries let mut msg = Message::new(Viewtype::File); - msg.set_text(some_text.clone()); - msg.param.set(Param::File, "foo.bar"); + msg.set_file_from_bytes(ctx, "autocrypt-setup-message.html", b"data", None) + .unwrap(); msg.param.set_cmd(SystemMessage::AutocryptSetupMessage); - assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages + assert_summary_texts(&msg, ctx, "📎 autocrypt-setup-message.html").await; + // no special handling of ASM } } diff --git a/src/sync.rs b/src/sync.rs index 4905ca0e5e..8708177487 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,7 @@ //! # Synchronize items between devices. use anyhow::Result; -use lettre_email::PartBuilder; +use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use crate::chat::{self, ChatId}; @@ -17,6 +17,7 @@ use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken}; use crate::token::Namespace; use crate::tools::time; use crate::{message, stock_str, token}; +use std::collections::HashSet; /// Whether to send device sync messages. Aimed for usage in the internal API. #[derive(Debug, PartialEq)] @@ -66,6 +67,9 @@ pub(crate) enum SyncData { src: String, // RFC724 id (i.e. "Message-Id" header) dest: String, // RFC724 id (i.e. "Message-Id" header) }, + DeleteMessages { + msgs: Vec, // RFC724 id (i.e. "Message-Id" header) + }, } #[derive(Debug, Serialize, Deserialize)] @@ -165,7 +169,7 @@ impl Context { /// /// Mustn't be called from multiple tasks in parallel to avoid sending the same sync items twice /// because sync items are removed from the db only after successful sending. We guarantee this - /// by calling `send_sync_msg()` only from the SMTP loop. + /// by calling `send_sync_msg()` only from the inbox loop. pub async fn send_sync_msg(&self) -> Result> { if let Some((json, ids)) = self.build_sync_json().await? { let chat_id = @@ -227,14 +231,8 @@ impl Context { } } - pub(crate) fn build_sync_part(&self, json: String) -> PartBuilder { - PartBuilder::new() - .content_type(&"application/json".parse::().unwrap()) - .header(( - "Content-Disposition", - "attachment; filename=\"multi-device-sync.json\"", - )) - .body(json) + pub(crate) fn build_sync_part(&self, json: String) -> MimePart<'static> { + MimePart::new("application/json", json).attachment("multi-device-sync.json") } /// Takes a JSON string created by `build_sync_json()` @@ -264,6 +262,7 @@ impl Context { AlterChat { id, action } => self.sync_alter_chat(id, action).await, SyncData::Config { key, val } => self.sync_config(key, val).await, SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await, + SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await, }, SyncDataOrUnknown::Unknown(data) => { warn!(self, "Ignored unknown sync item: {data}."); @@ -303,6 +302,26 @@ impl Context { } Ok(()) } + + async fn sync_message_deletion(&self, msgs: &Vec) -> Result<()> { + let mut modified_chat_ids = HashSet::new(); + let mut msg_ids = Vec::new(); + for rfc724_mid in msgs { + if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? { + if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? { + message::delete_msg_locally(self, &msg).await?; + msg_ids.push(msg.id); + modified_chat_ids.insert(msg.chat_id); + } else { + warn!(self, "Sync message delete: Database entry does not exist."); + } + } else { + warn!(self, "Sync message delete: {rfc724_mid:?} not found."); + } + } + message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?; + Ok(()) + } } #[cfg(test)] @@ -593,7 +612,7 @@ mod tests { // the same sync message sent to bob must not be executed let bob = TestContext::new_bob().await; - bob.recv_msg(&sent_msg).await; + bob.recv_msg_trash(&sent_msg).await; assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await?); Ok(()) @@ -724,10 +743,7 @@ mod tests { let fiona = &tcm.fiona().await; tcm.exec_securejoin_qr(fiona, alice2, &qr).await; let msg = fiona.get_last_msg().await; - assert_eq!( - msg.text, - "Member Me (fiona@example.net) added by alice@example.org." - ); + assert_eq!(msg.text, "Member Me added by alice@example.org."); Ok(()) } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 8beba66335..8ee9a1c74f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -6,14 +6,13 @@ use std::fmt::Write; use std::ops::{Deref, DerefMut}; use std::panic; use std::path::Path; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant}; use async_channel::{self as channel, Receiver, Sender}; use chat::ChatItem; use deltachat_contact_tools::{ContactAddress, EmailAddress}; use nu_ansi_term::Color; -use once_cell::sync::Lazy; use pretty_assertions::assert_eq; use rand::Rng; use tempfile::{tempdir, TempDir}; @@ -29,11 +28,11 @@ use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::constants::DC_GCL_NO_SPECIALS; use crate::constants::{Blocked, Chattype}; -use crate::contact::{Contact, ContactId, Modifier, Origin}; +use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::events::{Event, EventEmitter, EventType, Events}; -use crate::key::{self, DcKey, KeyPairUse}; +use crate::key::{self, DcKey, DcSecretKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::peerstate::Peerstate; @@ -47,8 +46,8 @@ use crate::tools::time; pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png"); /// Map of context IDs to names for [`TestContext`]s. -static CONTEXT_NAMES: Lazy>> = - Lazy::new(|| std::sync::RwLock::new(BTreeMap::new())); +static CONTEXT_NAMES: LazyLock>> = + LazyLock::new(|| std::sync::RwLock::new(BTreeMap::new())); /// Manage multiple [`TestContext`]s in one place. /// @@ -81,6 +80,30 @@ impl TestContextManager { .await } + pub async fn charlie(&mut self) -> TestContext { + TestContext::builder() + .configure_charlie() + .with_log_sink(self.log_sink.clone()) + .build() + .await + } + + pub async fn dom(&mut self) -> TestContext { + TestContext::builder() + .configure_dom() + .with_log_sink(self.log_sink.clone()) + .build() + .await + } + + pub async fn elena(&mut self) -> TestContext { + TestContext::builder() + .configure_elena() + .with_log_sink(self.log_sink.clone()) + .build() + .await + } + pub async fn fiona(&mut self) -> TestContext { TestContext::builder() .configure_fiona() @@ -167,7 +190,10 @@ impl TestContextManager { ); } - pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) { + /// Executes SecureJoin protocol between `scanner` and `scanned`. + /// + /// Returns chat ID of the 1:1 chat for `scanner`. + pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) -> ChatId { self.section(&format!( "{} scans {}'s QR code", scanner.name(), @@ -175,12 +201,20 @@ impl TestContextManager { )); let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap(); - self.exec_securejoin_qr(scanner, scanned, &qr).await; + self.exec_securejoin_qr(scanner, scanned, &qr).await } /// Executes SecureJoin initiated by `scanner` scanning `qr` generated by `scanned`. - pub async fn exec_securejoin_qr(&self, scanner: &TestContext, scanned: &TestContext, qr: &str) { - join_securejoin(&scanner.ctx, qr).await.unwrap(); + /// + /// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1 + /// chat with `scanned`, for a SecureJoin QR this is the group chat. + pub async fn exec_securejoin_qr( + &self, + scanner: &TestContext, + scanned: &TestContext, + qr: &str, + ) -> ChatId { + let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap(); loop { if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await { @@ -191,6 +225,7 @@ impl TestContextManager { break; } } + chat_id } } @@ -227,6 +262,27 @@ impl TestContextBuilder { self.with_key_pair(bob_keypair()) } + /// Configures as charlie@example.net with fixed secret key. + /// + /// This is a shortcut for `.with_key_pair(charlie_keypair())`. + pub fn configure_charlie(self) -> Self { + self.with_key_pair(charlie_keypair()) + } + + /// Configures as dom@example.net with fixed secret key. + /// + /// This is a shortcut for `.with_key_pair(dom_keypair())`. + pub fn configure_dom(self) -> Self { + self.with_key_pair(dom_keypair()) + } + + /// Configures as elena@example.net with fixed secret key. + /// + /// This is a shortcut for `.with_key_pair(elena_keypair())`. + pub fn configure_elena(self) -> Self { + self.with_key_pair(elena_keypair()) + } + /// Configures as fiona@example.net with fixed secret key. /// /// This is a shortcut for `.with_key_pair(fiona_keypair())`. @@ -271,7 +327,7 @@ impl TestContextBuilder { let test_context = TestContext::new_internal(Some(name), self.log_sink).await; test_context.configure_addr(&addr).await; - key::store_self_keypair(&test_context, &key_pair, KeyPairUse::Default) + key::store_self_keypair(&test_context, &key_pair) .await .expect("Failed to save key"); test_context @@ -400,11 +456,11 @@ impl TestContext { /// Sets a name for this [`TestContext`] if one isn't yet set. /// /// This will show up in events logged in the test output. - pub fn set_name(&self, name: impl Into) { + pub fn set_name(&self, name: &str) { let mut context_names = CONTEXT_NAMES.write().unwrap(); context_names .entry(self.ctx.get_id()) - .or_insert_with(|| name.into()); + .or_insert_with(|| name.to_string()); } /// Returns the name of this [`TestContext`]. @@ -421,15 +477,11 @@ impl TestContext { /// The context will be configured but the key will not be pre-generated so if a key is /// used the fingerprint will be different every time. pub async fn configure_addr(&self, addr: &str) { - self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap(); self.ctx .set_config(Config::ConfiguredAddr, Some(addr)) .await .unwrap(); - self.ctx - .set_config(Config::Configured, Some("1")) - .await - .unwrap(); + if let Some(name) = addr.split('@').next() { self.set_name(name); } @@ -607,6 +659,19 @@ impl TestContext { msg } + /// Receive a message using the `receive_imf()` pipeline. Panics if it's not hidden. + pub async fn recv_msg_hidden(&self, msg: &SentMessage<'_>) -> Message { + let received = self + .recv_msg_opt(msg) + .await + .expect("receive_imf() seems not to have added a new message to the db"); + let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap()) + .await + .unwrap(); + assert!(msg.hidden); + msg + } + /// Receive a message using the `receive_imf()` pipeline. This is similar /// to `recv_msg()`, but doesn't assume that the message is shown in the chat. pub async fn recv_msg_opt( @@ -657,7 +722,7 @@ impl TestContext { } /// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary. - pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId { + pub async fn add_or_lookup_email_contact_id(&self, other: &TestContext) -> ContactId { let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); let addr = ContactAddress::new(&primary_self_addr).unwrap(); // MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the @@ -674,6 +739,20 @@ impl TestContext { contact_id } + /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. + pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact { + let contact_id = self.add_or_lookup_email_contact_id(other).await; + Contact::get_by_id(&self.ctx, contact_id).await.unwrap() + } + + /// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary. + pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId { + let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap(); + let contact_ids = import_vcard(self, &vcard).await.unwrap(); + assert_eq!(contact_ids.len(), 1); + *contact_ids.first().unwrap() + } + /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact { let contact_id = self.add_or_lookup_contact_id(other).await; @@ -686,7 +765,7 @@ impl TestContext { /// This first creates a contact using the configured details on the other account, then /// gets the 1:1 chat with this contact. pub async fn get_chat(&self, other: &TestContext) -> Chat { - let contact = self.add_or_lookup_contact(other).await; + let contact = self.add_or_lookup_email_contact(other).await; let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id) .await @@ -702,11 +781,26 @@ impl TestContext { /// Creates or returns an existing 1:1 [`Chat`] with another account. /// - /// This first creates a contact using the configured details on the other account, then - /// creates a 1:1 chat with this contact. + /// This first creates a contact by exporting a vCard from the `other` + /// and importing it into `self`, + /// then creates a 1:1 chat with this contact. pub async fn create_chat(&self, other: &TestContext) -> Chat { - let contact = self.add_or_lookup_contact(other).await; + let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap(); + let contact_ids = import_vcard(self, &vcard).await.unwrap(); + assert_eq!(contact_ids.len(), 1); + let contact_id = contact_ids.first().unwrap(); + let chat_id = ChatId::create_for_contact(self, *contact_id).await.unwrap(); + Chat::load_from_db(self, chat_id).await.unwrap() + } + + /// Creates or returns an existing 1:1 [`Chat`] with another account + /// by email address. + /// + /// This function can be used to create unencrypted chats. + pub async fn create_email_chat(&self, other: &TestContext) -> Chat { + let contact = self.add_or_lookup_email_contact(other).await; let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap(); + Chat::load_from_db(self, chat_id).await.unwrap() } @@ -730,6 +824,15 @@ impl TestContext { Chat::load_from_db(self, chat_id).await.unwrap() } + pub async fn assert_no_chat(&self, id: ChatId) { + assert!(Chat::load_from_db(self, id).await.is_err()); + assert!(!self + .sql + .exists("SELECT COUNT(*) FROM chats WHERE id=?", (id,)) + .await + .unwrap()); + } + /// Sends out the text message. /// /// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call @@ -1033,12 +1136,10 @@ impl SentMessage<'_> { /// /// The keypair was created using the crate::key::tests::gen_key test. pub fn alice_keypair() -> KeyPair { - let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/alice-public.asc")) - .unwrap() - .0; let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")) .unwrap() .0; + let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1046,25 +1147,55 @@ pub fn alice_keypair() -> KeyPair { /// /// Like [alice_keypair] but a different key and identity. pub fn bob_keypair() -> KeyPair { - let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/bob-public.asc")) + let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")) .unwrap() .0; - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")) + let public = secret.split_public_key().unwrap(); + KeyPair { public, secret } +} + +/// Load a pre-generated keypair for charlie@example.net from disk. +/// +/// Like [alice_keypair] but a different key and identity. +pub fn charlie_keypair() -> KeyPair { + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc")) + .unwrap() + .0; + let public = secret.split_public_key().unwrap(); + KeyPair { public, secret } +} + +/// Load a pre-generated keypair for dom@example.net from disk. +/// +/// Like [alice_keypair] but a different key and identity. +pub fn dom_keypair() -> KeyPair { + let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")) .unwrap() .0; + let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } -/// Load a pre-generated keypair for fiona@example.net from disk. +/// Load a pre-generated keypair for elena@example.net from disk. /// /// Like [alice_keypair] but a different key and identity. -pub fn fiona_keypair() -> KeyPair { - let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc")) +pub fn elena_keypair() -> KeyPair { + let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")) .unwrap() .0; + let public = secret.split_public_key().unwrap(); + KeyPair { public, secret } +} + +/// Load a pre-generated keypair for fiona@example.net from disk. +/// +/// Like [alice_keypair] but a different key and identity. +pub fn fiona_keypair() -> KeyPair { let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")) .unwrap() .0; + let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index aa46671d74..4855470c6d 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -1,7 +1,9 @@ use anyhow::Result; use pretty_assertions::assert_eq; -use crate::chat::{self, add_contact_to_chat, Chat, ProtectionStatus}; +use crate::chat::{ + self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus, +}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING}; @@ -186,14 +188,14 @@ async fn test_missing_peerstate_reexecute_securejoin() -> Result<()> { let alice_addr = alice.get_config(Config::Addr).await?.unwrap(); let bob = &tcm.bob().await; enable_verified_oneonone_chats(&[alice, bob]).await; - tcm.execute_securejoin(bob, alice).await; - let chat = bob.get_chat(alice).await; + let chat_id = tcm.execute_securejoin(bob, alice).await; + let chat = Chat::load_from_db(bob, chat_id).await?; assert!(chat.is_protected()); bob.sql .execute("DELETE FROM acpeerstates WHERE addr=?", (&alice_addr,)) .await?; - tcm.execute_securejoin(bob, alice).await; - let chat = bob.get_chat(alice).await; + let chat_id = tcm.execute_securejoin(bob, alice).await; + let chat = Chat::load_from_db(bob, chat_id).await?; assert!(chat.is_protected()); assert!(!chat.is_protection_broken()); Ok(()) @@ -953,6 +955,67 @@ async fn test_no_unencrypted_name_if_encrypted() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_verified_lost_member_added() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; + + tcm.execute_securejoin(bob, alice).await; + tcm.execute_securejoin(fiona, alice).await; + + let alice_chat_id = alice + .create_group_with_members(ProtectionStatus::Protected, "Group", &[bob]) + .await; + let alice_sent = alice.send_text(alice_chat_id, "Hi!").await; + let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id; + assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + + // Attempt to add member, but message is lost. + let fiona_id = alice.add_or_lookup_contact(fiona).await.id; + add_contact_to_chat(alice, alice_chat_id, fiona_id).await?; + alice.pop_sent_msg().await; + + let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await; + bob.recv_msg(&alice_sent).await; + assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 3); + + bob_chat_id.accept(bob).await?; + let sent = bob.send_text(bob_chat_id, "Hello!").await; + let sent_msg = Message::load_from_db(bob, sent.sender_msg_id).await?; + assert_eq!(sent_msg.get_showpadlock(), true); + + // The message will not be sent to Fiona. + // Test that Fiona will not be able to decrypt it + // and the message is trashed because + // we don't create groups from undecipherable messages. + fiona.recv_msg_trash(&sent).await; + + // Advance the time so Alice does not leave at the same second + // as the group was created. + SystemTime::shift(std::time::Duration::from_secs(100)); + + // Alice leaves the chat. + remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?; + assert_eq!( + chat::get_chat_contacts(alice, alice_chat_id).await?.len(), + 2 + ); + bob.recv_msg(&alice.pop_sent_msg().await).await; + + // Now only Bob and Fiona are in the chat. + assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2); + + // Bob cannot send messages anymore because there are no recipients + // other than self for which Bob has the key. + let mut msg = Message::new_text("No key for Fiona".to_string()); + let result = send_msg(bob, bob_chat_id, &mut msg).await; + assert!(result.is_err()); + + Ok(()) +} + // ============== Helper Functions ============== async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) { diff --git a/src/tools.rs b/src/tools.rs index 9cb6d05d28..58b176c21b 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -34,6 +34,7 @@ use num_traits::PrimInt; use rand::{thread_rng, Rng}; use tokio::{fs, io}; use url::Url; +use uuid::Uuid; use crate::chat::{add_device_msg, add_device_msg_with_importance}; use crate::config::Config; @@ -178,7 +179,7 @@ pub(crate) fn gm2local_offset() -> i64 { /// Returns the current smeared timestamp, /// -/// The returned timestamp MUST NOT be sent out. +/// The returned timestamp MAY NOT be unique and MUST NOT go to "Date" header. pub(crate) fn smeared_time(context: &Context) -> i64 { let now = time(); let ts = context.smeared_timestamp.current(); @@ -275,12 +276,10 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time } } -/* Message-ID tools */ - /// Generate an unique ID. /// /// The generated ID should be short but unique: -/// - short, because it used in Message-ID and Chat-Group-ID headers and in QR codes +/// - short, because it used in Chat-Group-ID headers and in QR codes /// - unique as two IDs generated on two devices should not be the same /// /// IDs generated by this function have 144 bits of entropy @@ -312,7 +311,15 @@ pub(crate) fn validate_id(s: &str) -> bool { /// - the message ID should be globally unique /// - do not add a counter or any private data as this leaks information unnecessarily pub(crate) fn create_outgoing_rfc724_mid() -> String { - format!("{}@localhost", create_id()) + // We use UUID similarly to iCloud web mail client + // because it seems their spam filter does not like Message-IDs + // without hyphens. + // + // However, we use `localhost` instead of the real domain to avoid + // leaking the domain when resent by otherwise anonymizing + // From-rewriting mailing lists and forwarders. + let uuid = Uuid::new_v4(); + format!("{uuid}@localhost") } // the returned suffix is lower-case @@ -341,14 +348,13 @@ pub(crate) fn get_abs_path(context: &Context, path: &Path) -> PathBuf { } } -pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> Result { - let path_abs = get_abs_path(context, path.as_ref()); +pub(crate) async fn get_filebytes(context: &Context, path: &Path) -> Result { + let path_abs = get_abs_path(context, path); let meta = fs::metadata(&path_abs).await?; Ok(meta.len()) } -pub(crate) async fn delete_file(context: &Context, path: impl AsRef) -> Result<()> { - let path = path.as_ref(); +pub(crate) async fn delete_file(context: &Context, path: &Path) -> Result<()> { let path_abs = get_abs_path(context, path); if !path_abs.exists() { bail!("path {} does not exist", path_abs.display()); @@ -366,6 +372,41 @@ pub(crate) async fn delete_file(context: &Context, path: impl AsRef) -> Re Ok(()) } +/// Create a safe name based on a messy input string. +/// +/// The safe name will be a valid filename on Unix and Windows and +/// not contain any path separators. The input can contain path +/// segments separated by either Unix or Windows path separators, +/// the rightmost non-empty segment will be used as name, +/// sanitised for special characters. +pub(crate) fn sanitize_filename(mut name: &str) -> String { + for part in name.rsplit('/') { + if !part.is_empty() { + name = part; + break; + } + } + for part in name.rsplit('\\') { + if !part.is_empty() { + name = part; + break; + } + } + + let opts = sanitize_filename::Options { + truncate: true, + windows: true, + replacement: "", + }; + let name = sanitize_filename::sanitize_with_options(name, opts); + + if name.starts_with('.') || name.is_empty() { + format!("file{name}") + } else { + name + } +} + /// A guard which will remove the path when dropped. /// /// It implements [`Deref`] so it can be used as a `&Path`. @@ -401,11 +442,8 @@ impl AsRef for TempPathGuard { } } -pub(crate) async fn create_folder( - context: &Context, - path: impl AsRef, -) -> Result<(), io::Error> { - let path_abs = get_abs_path(context, path.as_ref()); +pub(crate) async fn create_folder(context: &Context, path: &Path) -> Result<(), io::Error> { + let path_abs = get_abs_path(context, path); if !path_abs.exists() { match fs::create_dir_all(path_abs).await { Ok(_) => Ok(()), @@ -413,7 +451,7 @@ pub(crate) async fn create_folder( warn!( context, "Cannot create directory \"{}\": {}", - path.as_ref().display(), + path.display(), err ); Err(err) @@ -427,16 +465,16 @@ pub(crate) async fn create_folder( /// Write a the given content to provided file path. pub(crate) async fn write_file( context: &Context, - path: impl AsRef, + path: &Path, buf: &[u8], ) -> Result<(), io::Error> { - let path_abs = get_abs_path(context, path.as_ref()); + let path_abs = get_abs_path(context, path); fs::write(&path_abs, buf).await.map_err(|err| { warn!( context, "Cannot write {} bytes to \"{}\": {}", buf.len(), - path.as_ref().display(), + path.display(), err ); err @@ -444,8 +482,8 @@ pub(crate) async fn write_file( } /// Reads the file and returns its context as a byte vector. -pub async fn read_file(context: &Context, path: impl AsRef) -> Result> { - let path_abs = get_abs_path(context, path.as_ref()); +pub async fn read_file(context: &Context, path: &Path) -> Result> { + let path_abs = get_abs_path(context, path); match fs::read(&path_abs).await { Ok(bytes) => Ok(bytes), @@ -453,7 +491,7 @@ pub async fn read_file(context: &Context, path: impl AsRef) -> Result) -> Result) -> Result { - let path_abs = get_abs_path(context, path.as_ref()); +pub async fn open_file(context: &Context, path: &Path) -> Result { + let path_abs = get_abs_path(context, path); match fs::File::open(&path_abs).await { Ok(bytes) => Ok(bytes), @@ -470,7 +508,7 @@ pub async fn open_file(context: &Context, path: impl AsRef) -> Result { + fn to_option(self) -> Option; +} +impl<'a> ToOption<&'a str> for &'a String { + fn to_option(self) -> Option<&'a str> { + if self.is_empty() { + None + } else { + Some(self) + } + } +} +impl ToOption for u16 { + fn to_option(self) -> Option { + if self == 0 { + None + } else { + Some(self.to_string()) + } + } +} +impl ToOption for Option { + fn to_option(self) -> Option { + match self { + None | Some(0) => None, + Some(v) => Some(v.to_string()), + } + } +} + pub fn remove_subject_prefix(last_subject: &str) -> String { let subject_start = if last_subject.starts_with("Chat:") { 0 @@ -698,566 +766,4 @@ pub(crate) fn inc_and_check( } #[cfg(test)] -mod tests { - use chrono::NaiveDate; - use proptest::prelude::*; - - use super::*; - use crate::chatlist::Chatlist; - use crate::{chat, test_utils}; - use crate::{receive_imf::receive_imf, test_utils::TestContext}; - - #[test] - fn test_parse_receive_headers() { - // Test `parse_receive_headers()` with some more-or-less random emails from the test-data - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let expected = - "Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\ - Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; - check_parse_receive_headers(raw, expected); - - let raw = include_bytes!("../test-data/message/wrong-html.eml"); - let expected = - "Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\ - Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000"; - check_parse_receive_headers(raw, expected); - - let raw = include_bytes!("../test-data/message/posteo_ndn.eml"); - let expected = - "Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ - Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ - Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ - Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000"; - check_parse_receive_headers(raw, expected); - } - - fn check_parse_receive_headers(raw: &[u8], expected: &str) { - let mail = mailparse::parse_mail(raw).unwrap(); - let hop_info = parse_receive_headers(&mail.get_headers()); - assert_eq!(hop_info, expected) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_receive_headers_integration() { - let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); - let expected = r"State: Fresh - -hi - -Message-ID: 2dfdbde7@example.org - -Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 -Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000 - -DKIM Results: Passed=true"; - check_parse_receive_headers_integration(raw, expected).await; - - let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml"); - let expected = "State: Fresh, Encrypted - -Re: Message from alice@example.org - -hi back\r\n\ -\r\n\ --- \r\n\ -Sent with my Delta Chat Messenger: https://delta.chat - -Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net - -Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 -Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 -Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 - -DKIM Results: Passed=true"; - check_parse_receive_headers_integration(raw, expected).await; - } - - async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) { - let t = TestContext::new_alice().await; - receive_imf(&t, raw, false).await.unwrap(); - let msg = t.get_last_msg().await; - let msg_info = msg.id.get_info(&t).await.unwrap(); - - // Ignore the first rows of the msg_info because they contain a - // received time that depends on the test time which makes it impossible to - // compare with a static string - let capped_result = &msg_info[msg_info.find("State").unwrap()..]; - assert_eq!(expected, capped_result); - } - - #[test] - fn test_rust_ftoa() { - assert_eq!("1.22", format!("{}", 1.22)); - } - - #[test] - fn test_truncate_1() { - let s = "this is a little test string"; - assert_eq!(truncate(s, 16), "this is a [...]"); - } - - #[test] - fn test_truncate_2() { - assert_eq!(truncate("1234", 2), "1234"); - } - - #[test] - fn test_truncate_3() { - assert_eq!(truncate("1234567", 1), "1[...]"); - } - - #[test] - fn test_truncate_4() { - assert_eq!(truncate("123456", 4), "123456"); - } - - #[test] - fn test_truncate_edge() { - assert_eq!(truncate("", 4), ""); - - assert_eq!(truncate("\n hello \n world", 4), "\n [...]"); - - assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]"); - assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]"); - - // 9 characters, so no truncation - assert_eq!(truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",); - - // 12 characters, truncation - assert_eq!( - truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6), - "𑒀ὐ¢🜀\u{1e01b}A[...]", - ); - } - - mod truncate_by_lines { - use super::*; - - #[test] - fn test_just_text() { - let s = "this is a little test string".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 6), - ("this is a little test [...]".to_string(), true) - ); - } - - #[test] - fn test_with_linebreaks() { - let s = "this\n is\n a little test string".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 6), - ("this\n is\n a little [...]".to_string(), true) - ); - } - - #[test] - fn test_only_linebreaks() { - let s = "\n\n\n\n\n\n\n".to_string(); - assert_eq!( - truncate_by_lines(s, 4, 5), - ("\n\n\n[...]".to_string(), true) - ); - } - - #[test] - fn limit_hits_end() { - let s = "hello\n world !".to_string(); - assert_eq!( - truncate_by_lines(s, 2, 8), - ("hello\n world !".to_string(), false) - ); - } - - #[test] - fn test_edge() { - assert_eq!( - truncate_by_lines("".to_string(), 2, 4), - ("".to_string(), false) - ); - - assert_eq!( - truncate_by_lines("\n hello \n world".to_string(), 2, 4), - ("\n [...]".to_string(), true) - ); - assert_eq!( - truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 2), - ("𐠈0[...]".to_string(), true) - ); - assert_eq!( - truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 0), - ("[...]".to_string(), true) - ); - - // 9 characters, so no truncation - assert_eq!( - truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), 1, 12), - ("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), false), - ); - - // 12 characters, truncation - assert_eq!( - truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd".to_string(), 1, 7), - ("𑒀ὐ¢🜀\u{1e01b}A [...]".to_string(), true), - ); - } - } - - #[test] - fn test_create_id() { - let buf = create_id(); - assert_eq!(buf.len(), 24); - } - - #[test] - fn test_validate_id() { - for _ in 0..10 { - assert!(validate_id(&create_id())); - } - - assert_eq!(validate_id("aaaaaaaaaaaa"), true); - assert_eq!(validate_id("aa-aa_aaaXaa"), true); - - // ID cannot contain whitespace. - assert_eq!(validate_id("aaaaa aaaaaa"), false); - assert_eq!(validate_id("aaaaa\naaaaaa"), false); - - // ID cannot contain "/", "+". - assert_eq!(validate_id("aaaaa/aaaaaa"), false); - assert_eq!(validate_id("aaaaaaaa+aaa"), false); - - // Too long ID. - assert_eq!(validate_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), false); - } - - #[test] - fn test_create_id_invalid_chars() { - for _ in 1..1000 { - let buf = create_id(); - assert!(!buf.contains('/')); // `/` must not be used to be URL-safe - assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID - } - } - - #[test] - fn test_create_outgoing_rfc724_mid() { - let mid = create_outgoing_rfc724_mid(); - assert_eq!(mid.len(), 34); - assert!(mid.ends_with("@localhost")); - } - - proptest! { - #[test] - fn test_truncate( - buf: String, - approx_chars in 0..100usize - ) { - let res = truncate(&buf, approx_chars); - let el_len = 5; - let l = res.chars().count(); - assert!( - l <= approx_chars + el_len, - "buf: '{}' - res: '{}' - len {}, approx {}", - &buf, &res, res.len(), approx_chars - ); - - if buf.chars().count() > approx_chars + el_len { - let l = res.len(); - assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res); - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_file_handling() { - let t = TestContext::new().await; - let context = &t; - macro_rules! file_exist { - ($ctx:expr, $fname:expr) => { - $ctx.get_blobdir() - .join(Path::new($fname).file_name().unwrap()) - .exists() - }; - } - - assert!(delete_file(context, "$BLOBDIR/lkqwjelqkwlje") - .await - .is_err()); - assert!(write_file(context, "$BLOBDIR/foobar", b"content") - .await - .is_ok()); - assert!(file_exist!(context, "$BLOBDIR/foobar")); - assert!(!file_exist!(context, "$BLOBDIR/foobarx")); - assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7); - - let abs_path = context - .get_blobdir() - .join("foobar") - .to_string_lossy() - .to_string(); - - assert!(file_exist!(context, &abs_path)); - - assert!(delete_file(context, "$BLOBDIR/foobar").await.is_ok()); - assert!(create_folder(context, "$BLOBDIR/foobar-folder") - .await - .is_ok()); - assert!(file_exist!(context, "$BLOBDIR/foobar-folder")); - assert!(delete_file(context, "$BLOBDIR/foobar-folder") - .await - .is_err()); - - let fn0 = "$BLOBDIR/data.data"; - assert!(write_file(context, &fn0, b"content").await.is_ok()); - - assert!(delete_file(context, &fn0).await.is_ok()); - assert!(!file_exist!(context, &fn0)); - } - - #[test] - fn test_duration_to_str() { - assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s"); - assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s"); - assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s"); - assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s"); - assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s"); - assert_eq!( - duration_to_str(Duration::from_secs(59 * 60 + 59)), - "0h 59m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(59 * 60 + 60)), - "1h 0m 0s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)), - "2h 59m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)), - "3h 0m 0s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)), - "3h 0m 59s" - ); - assert_eq!( - duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)), - "3h 1m 0s" - ); - } - - #[test] - fn test_get_filemeta() { - let (w, h) = get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap(); - assert_eq!(w, 900); - assert_eq!(h, 900); - - let data = include_bytes!("../test-data/image/avatar1000x1000.jpg"); - let (w, h) = get_filemeta(data).unwrap(); - assert_eq!(w, 1000); - assert_eq!(h, 1000); - - let data = include_bytes!("../test-data/image/image100x50.gif"); - let (w, h) = get_filemeta(data).unwrap(); - assert_eq!(w, 100); - assert_eq!(h, 50); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_maybe_warn_on_bad_time() { - let t = TestContext::new().await; - let timestamp_now = time(); - let timestamp_future = timestamp_now + 60 * 60 * 24 * 7; - let timestamp_past = NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap(), - ) - .and_utc() - .timestamp_millis() - / 1_000; - - // a correct time must not add a device message - maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // we cannot find out if a date in the future is wrong - a device message is not added - maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // a date in the past must add a device message - maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // the message should be added only once a day - test that an hour later and nearly a day later - maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await; - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - maybe_warn_on_bad_time( - &t, - timestamp_past + 60 * 60 * 24 - 1, - get_release_timestamp(), - ) - .await; - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // next day, there should be another device message - maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap()); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_maybe_warn_on_outdated() { - let t = TestContext::new().await; - let timestamp_now: i64 = time(); - - // in about 6 months, the app should not be outdated - // (if this fails, provider-db is not updated since 6 months) - maybe_warn_on_outdated( - &t, - timestamp_now + 180 * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // in 1 year, the app should be considered as outdated - maybe_warn_on_outdated( - &t, - timestamp_now + 365 * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), 1); - - // do not repeat the warning every day ... - // (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message) - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 1) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 2) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - let test_len = msgs.len(); - assert!(test_len == 1 || test_len == 2); - - // ... but every month - // (forward generous 33 days to avoid being in the same month as in the previous check) - maybe_warn_on_outdated( - &t, - timestamp_now + (365 + 33) * 24 * 60 * 60, - get_release_timestamp(), - ) - .await; - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let device_chat_id = chats.get_chat_id(0).unwrap(); - let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - assert_eq!(msgs.len(), test_len + 1); - } - - #[test] - fn test_get_release_timestamp() { - let timestamp_past = NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap(), - ) - .and_utc() - .timestamp_millis() - / 1_000; - assert!(get_release_timestamp() <= time()); - assert!(get_release_timestamp() > timestamp_past); - } - - #[test] - fn test_remove_subject_prefix() { - assert_eq!(remove_subject_prefix("Subject"), "Subject"); - assert_eq!( - remove_subject_prefix("Chat: Re: Subject"), - "Chat: Re: Subject" - ); - assert_eq!(remove_subject_prefix("Re: Subject"), "Subject"); - assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject"); - assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject"); - } - - #[test] - fn test_parse_mailto() { - let mailto_url = "mailto:someone@example.com"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }], - subject: None, - body: None - }), - reps - ); - - let mailto_url = "mailto:someone@example.com?subject=Hello%20World"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }], - subject: Some("Hello World".to_string()), - body: None - }), - reps - ); - - let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test"; - let reps = parse_mailto(mailto_url); - assert_eq!( - Some(MailTo { - to: vec![ - EmailAddress { - local: "someone".to_string(), - domain: "example.com".to_string() - }, - EmailAddress { - local: "someoneelse".to_string(), - domain: "example.com".to_string() - } - ], - subject: Some("Hello World".to_string()), - body: Some("This is a test".to_string()) - }), - reps - ); - } -} +mod tools_tests; diff --git a/src/tools/tools_tests.rs b/src/tools/tools_tests.rs new file mode 100644 index 0000000000..b2b8fe36cc --- /dev/null +++ b/src/tools/tools_tests.rs @@ -0,0 +1,599 @@ +use chrono::NaiveDate; +use proptest::prelude::*; + +use super::*; +use crate::chatlist::Chatlist; +use crate::{chat, test_utils}; +use crate::{receive_imf::receive_imf, test_utils::TestContext}; + +#[test] +fn test_parse_receive_headers() { + // Test `parse_receive_headers()` with some more-or-less random emails from the test-data + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let expected = + "Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\ + Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../../test-data/message/wrong-html.eml"); + let expected = + "Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\ + Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000"; + check_parse_receive_headers(raw, expected); + + let raw = include_bytes!("../../test-data/message/posteo_ndn.eml"); + let expected = + "Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ + Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\ + Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\ + Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000"; + check_parse_receive_headers(raw, expected); +} + +fn check_parse_receive_headers(raw: &[u8], expected: &str) { + let mail = mailparse::parse_mail(raw).unwrap(); + let hop_info = parse_receive_headers(&mail.get_headers()); + assert_eq!(hop_info, expected) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_receive_headers_integration() { + let raw = include_bytes!("../../test-data/message/mail_with_cc.txt"); + let expected = r"State: Fresh + +Message-ID: 2dfdbde7@example.org + +Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 +Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000 + +DKIM Results: Passed=true"; + check_parse_receive_headers_integration(raw, expected).await; + + let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml"); + let expected = "State: Fresh, Encrypted + +Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net + +Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 +Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 +Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 + +DKIM Results: Passed=true"; + check_parse_receive_headers_integration(raw, expected).await; +} + +async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) { + let t = TestContext::new_alice().await; + receive_imf(&t, raw, false).await.unwrap(); + let msg = t.get_last_msg().await; + let msg_info = msg.id.get_info(&t).await.unwrap(); + + // Ignore the first rows of the msg_info because they contain a + // received time that depends on the test time which makes it impossible to + // compare with a static string + let capped_result = &msg_info[msg_info.find("State").unwrap()..]; + assert_eq!(expected, capped_result); +} + +#[test] +fn test_rust_ftoa() { + assert_eq!("1.22", format!("{}", 1.22)); +} + +#[test] +fn test_truncate_1() { + let s = "this is a little test string"; + assert_eq!(truncate(s, 16), "this is a [...]"); +} + +#[test] +fn test_truncate_2() { + assert_eq!(truncate("1234", 2), "1234"); +} + +#[test] +fn test_truncate_3() { + assert_eq!(truncate("1234567", 1), "1[...]"); +} + +#[test] +fn test_truncate_4() { + assert_eq!(truncate("123456", 4), "123456"); +} + +#[test] +fn test_truncate_edge() { + assert_eq!(truncate("", 4), ""); + + assert_eq!(truncate("\n hello \n world", 4), "\n [...]"); + + assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]"); + assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]"); + + // 9 characters, so no truncation + assert_eq!(truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",); + + // 12 characters, truncation + assert_eq!( + truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6), + "𑒀ὐ¢🜀\u{1e01b}A[...]", + ); +} + +mod truncate_by_lines { + use super::*; + + #[test] + fn test_just_text() { + let s = "this is a little test string".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 6), + ("this is a little test [...]".to_string(), true) + ); + } + + #[test] + fn test_with_linebreaks() { + let s = "this\n is\n a little test string".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 6), + ("this\n is\n a little [...]".to_string(), true) + ); + } + + #[test] + fn test_only_linebreaks() { + let s = "\n\n\n\n\n\n\n".to_string(); + assert_eq!( + truncate_by_lines(s, 4, 5), + ("\n\n\n[...]".to_string(), true) + ); + } + + #[test] + fn limit_hits_end() { + let s = "hello\n world !".to_string(); + assert_eq!( + truncate_by_lines(s, 2, 8), + ("hello\n world !".to_string(), false) + ); + } + + #[test] + fn test_edge() { + assert_eq!( + truncate_by_lines("".to_string(), 2, 4), + ("".to_string(), false) + ); + + assert_eq!( + truncate_by_lines("\n hello \n world".to_string(), 2, 4), + ("\n [...]".to_string(), true) + ); + assert_eq!( + truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 2), + ("𐠈0[...]".to_string(), true) + ); + assert_eq!( + truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 0), + ("[...]".to_string(), true) + ); + + // 9 characters, so no truncation + assert_eq!( + truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), 1, 12), + ("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), false), + ); + + // 12 characters, truncation + assert_eq!( + truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd".to_string(), 1, 7), + ("𑒀ὐ¢🜀\u{1e01b}A [...]".to_string(), true), + ); + } +} + +#[test] +fn test_create_id() { + let buf = create_id(); + assert_eq!(buf.len(), 24); +} + +#[test] +fn test_validate_id() { + for _ in 0..10 { + assert!(validate_id(&create_id())); + } + + assert_eq!(validate_id("aaaaaaaaaaaa"), true); + assert_eq!(validate_id("aa-aa_aaaXaa"), true); + + // ID cannot contain whitespace. + assert_eq!(validate_id("aaaaa aaaaaa"), false); + assert_eq!(validate_id("aaaaa\naaaaaa"), false); + + // ID cannot contain "/", "+". + assert_eq!(validate_id("aaaaa/aaaaaa"), false); + assert_eq!(validate_id("aaaaaaaa+aaa"), false); + + // Too long ID. + assert_eq!(validate_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), false); +} + +#[test] +fn test_create_id_invalid_chars() { + for _ in 1..1000 { + let buf = create_id(); + assert!(!buf.contains('/')); // `/` must not be used to be URL-safe + assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID + } +} + +#[test] +fn test_create_outgoing_rfc724_mid() { + let mid = create_outgoing_rfc724_mid(); + assert_eq!(mid.len(), 46); + assert!(mid.contains("-")); // It has an UUID inside. + assert!(mid.ends_with("@localhost")); +} + +proptest! { + #[test] + fn test_truncate( + buf: String, + approx_chars in 0..100usize + ) { + let res = truncate(&buf, approx_chars); + let el_len = 5; + let l = res.chars().count(); + assert!( + l <= approx_chars + el_len, + "buf: '{}' - res: '{}' - len {}, approx {}", + &buf, &res, res.len(), approx_chars + ); + + if buf.chars().count() > approx_chars + el_len { + let l = res.len(); + assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_file_handling() { + let t = TestContext::new().await; + let context = &t; + macro_rules! file_exist { + ($ctx:expr, $fname:expr) => { + $ctx.get_blobdir() + .join(Path::new($fname).file_name().unwrap()) + .exists() + }; + } + + assert!(delete_file(context, Path::new("$BLOBDIR/lkqwjelqkwlje")) + .await + .is_err()); + assert!( + write_file(context, Path::new("$BLOBDIR/foobar"), b"content") + .await + .is_ok() + ); + assert!(file_exist!(context, "$BLOBDIR/foobar")); + assert!(!file_exist!(context, "$BLOBDIR/foobarx")); + assert_eq!( + get_filebytes(context, Path::new("$BLOBDIR/foobar")) + .await + .unwrap(), + 7 + ); + + let abs_path = context + .get_blobdir() + .join("foobar") + .to_string_lossy() + .to_string(); + + assert!(file_exist!(context, &abs_path)); + + assert!(delete_file(context, Path::new("$BLOBDIR/foobar")) + .await + .is_ok()); + assert!(create_folder(context, Path::new("$BLOBDIR/foobar-folder")) + .await + .is_ok()); + assert!(file_exist!(context, "$BLOBDIR/foobar-folder")); + assert!(delete_file(context, Path::new("$BLOBDIR/foobar-folder")) + .await + .is_err()); + + let fn0 = "$BLOBDIR/data.data"; + assert!(write_file(context, Path::new(fn0), b"content") + .await + .is_ok()); + + assert!(delete_file(context, Path::new(fn0)).await.is_ok()); + assert!(!file_exist!(context, &fn0)); +} + +#[test] +fn test_duration_to_str() { + assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s"); + assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s"); + assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s"); + assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s"); + assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s"); + assert_eq!( + duration_to_str(Duration::from_secs(59 * 60 + 59)), + "0h 59m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(59 * 60 + 60)), + "1h 0m 0s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)), + "2h 59m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)), + "3h 0m 0s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)), + "3h 0m 59s" + ); + assert_eq!( + duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)), + "3h 1m 0s" + ); +} + +#[test] +fn test_get_filemeta() { + let (w, h) = get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap(); + assert_eq!(w, 900); + assert_eq!(h, 900); + + let data = include_bytes!("../../test-data/image/avatar1000x1000.jpg"); + let (w, h) = get_filemeta(data).unwrap(); + assert_eq!(w, 1000); + assert_eq!(h, 1000); + + let data = include_bytes!("../../test-data/image/image100x50.gif"); + let (w, h) = get_filemeta(data).unwrap(); + assert_eq!(w, 100); + assert_eq!(h, 50); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_warn_on_bad_time() { + let t = TestContext::new().await; + let timestamp_now = time(); + let timestamp_future = timestamp_now + 60 * 60 * 24 * 7; + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .and_utc() + .timestamp_millis() + / 1_000; + + // a correct time must not add a device message + maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // we cannot find out if a date in the future is wrong - a device message is not added + maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // a date in the past must add a device message + maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // the message should be added only once a day - test that an hour later and nearly a day later + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await; + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + maybe_warn_on_bad_time( + &t, + timestamp_past + 60 * 60 * 24 - 1, + get_release_timestamp(), + ) + .await; + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // next day, there should be another device message + maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap()); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_maybe_warn_on_outdated() { + let t = TestContext::new().await; + let timestamp_now: i64 = time(); + + // in about 6 months, the app should not be outdated + // (if this fails, provider-db is not updated since 6 months) + maybe_warn_on_outdated( + &t, + timestamp_now + 180 * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // in 1 year, the app should be considered as outdated + maybe_warn_on_outdated( + &t, + timestamp_now + 365 * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), 1); + + // do not repeat the warning every day ... + // (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message) + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 1) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 2) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + let test_len = msgs.len(); + assert!(test_len == 1 || test_len == 2); + + // ... but every month + // (forward generous 33 days to avoid being in the same month as in the previous check) + maybe_warn_on_outdated( + &t, + timestamp_now + (365 + 33) * 24 * 60 * 60, + get_release_timestamp(), + ) + .await; + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let device_chat_id = chats.get_chat_id(0).unwrap(); + let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + assert_eq!(msgs.len(), test_len + 1); +} + +#[test] +fn test_get_release_timestamp() { + let timestamp_past = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + .and_utc() + .timestamp_millis() + / 1_000; + assert!(get_release_timestamp() <= time()); + assert!(get_release_timestamp() > timestamp_past); +} + +#[test] +fn test_remove_subject_prefix() { + assert_eq!(remove_subject_prefix("Subject"), "Subject"); + assert_eq!( + remove_subject_prefix("Chat: Re: Subject"), + "Chat: Re: Subject" + ); + assert_eq!(remove_subject_prefix("Re: Subject"), "Subject"); + assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject"); + assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject"); +} + +#[test] +fn test_parse_mailto() { + let mailto_url = "mailto:someone@example.com"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }], + subject: None, + body: None + }), + reps + ); + + let mailto_url = "mailto:someone@example.com?subject=Hello%20World"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }], + subject: Some("Hello World".to_string()), + body: None + }), + reps + ); + + let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test"; + let reps = parse_mailto(mailto_url); + assert_eq!( + Some(MailTo { + to: vec![ + EmailAddress { + local: "someone".to_string(), + domain: "example.com".to_string() + }, + EmailAddress { + local: "someoneelse".to_string(), + domain: "example.com".to_string() + } + ], + subject: Some("Hello World".to_string()), + body: Some("This is a test".to_string()) + }), + reps + ); +} + +#[test] +fn test_sanitize_filename() { + let name = sanitize_filename("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt"); + assert!(!name.is_empty()); + + let name = sanitize_filename("wot.tar.gz"); + assert_eq!(name, "wot.tar.gz"); + + let name = sanitize_filename(".foo.bar"); + assert_eq!(name, "file.foo.bar"); + + let name = sanitize_filename("foo?.bar"); + assert_eq!(name, "foo.bar"); + assert!(!name.contains('?')); + + let name = sanitize_filename("no-extension"); + assert_eq!(name, "no-extension"); + + let name = sanitize_filename("path/ignored\\this: is* forbidden?.c"); + assert_eq!(name, "this is forbidden.c"); + + let name = + sanitize_filename("file.with_lots_of_characters_behind_point_and_double_ending.tar.gz"); + assert_eq!( + name, + "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" + ); + + let name = sanitize_filename("a. tar.tar.gz"); + assert_eq!(name, "a. tar.tar.gz"); + + let name = sanitize_filename("Guia_uso_GNB (v0.8).pdf"); + assert_eq!(name, "Guia_uso_GNB (v0.8).pdf"); +} diff --git a/src/update_helper.rs b/src/update_helper.rs index b343a272a7..2ba035c7df 100644 --- a/src/update_helper.rs +++ b/src/update_helper.rs @@ -213,6 +213,44 @@ mod tests { // Assert that the \n was correctly removed from the group name also in the system message assert_eq!(msg.text.contains('\n'), false); + // This doesn't update the name because Date is the same and name is greater. + receive_imf( + &t, + b"From: Bob Authname \n\ + To: alice@example.org\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: abcde123456\n\ + Chat-Group-Name: another name update 4\n\ + Chat-Group-Name-Changed: another name update\n\ + Date: Sun, 22 Mar 2021 03:00:00 +0000\n\ + \n\ + 4th message\n", + false, + ) + .await?; + let chat = Chat::load_from_db(&t, chat.id).await?; + assert_eq!(chat.name, "another name update"); + + // This updates the name because Date is the same and name is lower. + receive_imf( + &t, + b"From: Bob Authname \n\ + To: alice@example.org\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: abcde123456\n\ + Chat-Group-Name: another name updat\n\ + Chat-Group-Name-Changed: another name update\n\ + Date: Sun, 22 Mar 2021 03:00:00 +0000\n\ + \n\ + 5th message\n", + false, + ) + .await?; + let chat = Chat::load_from_db(&t, chat.id).await?; + assert_eq!(chat.name, "another name updat"); + Ok(()) } } diff --git a/src/webxdc.rs b/src/webxdc.rs index 5f93204743..ecc4d58f97 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -27,7 +27,7 @@ use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result}; use async_zip::tokio::read::seek::ZipFileReader as SeekZipFileReader; use deltachat_contact_tools::sanitize_bidi_characters; use deltachat_derive::FromSql; -use lettre_email::PartBuilder; +use mail_builder::mime::MimePart; use rusqlite::OptionalExtension; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -41,7 +41,6 @@ use crate::context::Context; use crate::events::EventType; use crate::key::{load_self_public_key, DcKey}; use crate::message::{Message, MessageState, MsgId, Viewtype}; -use crate::mimefactory::wrapped_base64_encode; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::mimeparser::SystemMessage; use crate::param::Param; @@ -389,6 +388,7 @@ impl Context { None, Some(&instance), Some(from_id), + None, ) .await?; } @@ -651,17 +651,8 @@ impl Context { } } - pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder { - let encoded_body = wrapped_base64_encode(json.as_bytes()); - - PartBuilder::new() - .content_type(&"application/json".parse::().unwrap()) - .header(( - "Content-Disposition", - "attachment; filename=\"status-update.json\"", - )) - .header(("Content-Transfer-Encoding", "base64")) - .body(encoded_body) + pub(crate) fn build_status_update_part(&self, json: &str) -> MimePart<'static> { + MimePart::new("application/json", json.as_bytes().to_vec()).attachment("status-update.json") } /// Receives status updates from receive_imf to the database @@ -974,7 +965,7 @@ impl Message { let fingerprint = load_self_public_key(context).await?.dc_fingerprint().hex(); let data = format!("{}-{}", fingerprint, self.rfc724_mid); let hash = Sha256::digest(data.as_bytes()); - Ok(format!("{:x}", hash)) + Ok(format!("{hash:x}")) } /// Get link attached to an info message. diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index 9586082d94..464e22f993 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -124,6 +124,23 @@ async fn test_send_invalid_webxdc() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_draft_invalid_webxdc() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + let mut instance = create_webxdc_instance( + &t, + "invalid-no-zip-but-7z.xdc", + include_bytes!("../../test-data/webxdc/invalid-no-zip-but-7z.xdc"), + )?; + + // draft should not fail + chat_id.set_draft(&t, Some(&mut instance)).await?; + chat_id.get_draft(&t).await.unwrap(); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_special_webxdc_format() -> Result<()> { let t = TestContext::new_alice().await; @@ -192,9 +209,10 @@ async fn test_forward_webxdc_instance() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_resend_webxdc_instance_and_info() -> Result<()> { let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; // Alice uses webxdc in a group - let alice = tcm.alice().await; alice.set_config_bool(Config::BccSelf, false).await?; let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; let alice_instance = send_webxdc_instance(&alice, alice_grp).await?; @@ -212,7 +230,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> { add_contact_to_chat( &alice, alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, + alice.add_or_lookup_contact_id(&bob).await, ) .await?; assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3); @@ -222,7 +240,6 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> { let sent2 = alice.pop_sent_msg().await; // Bob receives webxdc, legacy info-messages updates are received and added to the chat. - let bob = tcm.bob().await; let bob_instance = bob.recv_msg(&sent1).await; bob.recv_msg_trash(&sent2).await; assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); @@ -339,7 +356,6 @@ async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> { sent1.payload().as_bytes(), false, Some(70790), - false, ) .await?; let bob_instance = bob.get_last_msg().await; @@ -354,7 +370,6 @@ async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> { sent1.payload().as_bytes(), false, None, - false, ) .await? .unwrap(); @@ -726,7 +741,7 @@ async fn test_send_webxdc_status_update() -> Result<()> { let bob = TestContext::new_bob().await; // Alice sends an webxdc instance and a status update - let alice_chat = alice.create_chat(&bob).await; + let alice_chat = alice.create_email_chat(&bob).await; let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; let sent1 = &alice.pop_sent_msg().await; assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); @@ -1022,7 +1037,7 @@ async fn test_pop_status_update() -> Result<()> { async fn test_draft_and_send_webxdc_status_update() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - let alice_chat_id = alice.create_chat(&bob).await.id; + let alice_chat_id = alice.create_email_chat(&bob).await.id; // prepare webxdc instance, // status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None) @@ -1648,29 +1663,26 @@ async fn test_webxdc_no_internet_access() -> Result<()> { let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; let broadcast_id = create_broadcast_list(&t).await?; - for e2ee in ["1", "0"] { - t.set_config(Config::E2eeEnabled, Some(e2ee)).await?; - for chat_id in [self_id, single_id, group_id, broadcast_id] { - for internet_xdc in [true, false] { - let mut instance = create_webxdc_instance( - &t, - "foo.xdc", - if internet_xdc { - include_bytes!("../../test-data/webxdc/request-internet-access.xdc") - } else { - include_bytes!("../../test-data/webxdc/minimal.xdc") - }, - )?; - let instance_id = send_msg(&t, chat_id, &mut instance).await?; - t.send_webxdc_status_update( - instance_id, - r#"{"summary":"real summary", "payload": 42}"#, - ) - .await?; - let instance = Message::load_from_db(&t, instance_id).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.internet_access, false); - } + for chat_id in [self_id, single_id, group_id, broadcast_id] { + for internet_xdc in [true, false] { + let mut instance = create_webxdc_instance( + &t, + "foo.xdc", + if internet_xdc { + include_bytes!("../../test-data/webxdc/request-internet-access.xdc") + } else { + include_bytes!("../../test-data/webxdc/minimal.xdc") + }, + )?; + let instance_id = send_msg(&t, chat_id, &mut instance).await?; + t.send_webxdc_status_update( + instance_id, + r#"{"summary":"real summary", "payload": 42}"#, + ) + .await?; + let instance = Message::load_from_db(&t, instance_id).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.internet_access, false); } } @@ -1748,29 +1760,30 @@ async fn helper_send_receive_status_update( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - add_contact_to_chat(&alice, chat_id, contact_bob).await?; - let instance = send_webxdc_instance(&alice, chat_id).await?; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let contact_bob = alice.add_or_lookup_contact_id(bob).await; + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat(alice, chat_id, contact_bob).await?; + let instance = send_webxdc_instance(alice, chat_id).await?; bob.recv_msg(&alice.pop_sent_msg().await).await; let bob_instance = bob.get_last_msg().await; - Chat::load_from_db(&bob, bob_instance.chat_id) + Chat::load_from_db(bob, bob_instance.chat_id) .await? .id - .accept(&bob) + .accept(bob) .await?; - let status = helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; + let status = helper_send_receive_status_update(bob, alice, &bob_instance, &instance).await?; assert_eq!( status, r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# ); - remove_contact_from_chat(&alice, chat_id, contact_bob).await?; + remove_contact_from_chat(alice, chat_id, contact_bob).await?; alice.pop_sent_msg().await; - let status = helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; + let status = helper_send_receive_status_update(bob, alice, &bob_instance, &instance).await?; assert_eq!( status, @@ -2199,3 +2212,26 @@ async fn test_webxdc_href() -> Result<()> { Ok(()) } + +/// Test that the draft `selfAddr` is equal to the `selfAddr` of the sent message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_addr_consistency() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice_chat = alice + .create_group_with_members(ProtectionStatus::Unprotected, "No friends :(", &[]) + .await; + let mut instance = create_webxdc_instance( + alice, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + alice_chat.set_draft(alice, Some(&mut instance)).await?; + let mut instance = alice_chat.get_draft(alice).await?.unwrap(); + let self_addr = instance.get_webxdc_self_addr(alice).await?; + let sent = alice.send_msg(alice_chat, &mut instance).await; + let db_msg = Message::load_from_db(alice, sent.sender_msg_id).await?; + assert_eq!(db_msg.get_webxdc_self_addr(alice).await?, self_addr); + assert_eq!(alice_chat.get_msg_cnt(alice).await?, 1); + Ok(()) +} diff --git a/standards.md b/standards.md index 1038599e8a..36f07e4397 100644 --- a/standards.md +++ b/standards.md @@ -21,7 +21,7 @@ Detect/prevent active attacks | [securejoin][] protocols Compare public keys | [openpgp4fpr][] URI Scheme Header encryption | [Header Protection for Cryptographically Protected E-mail](https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/) Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][] -Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification) +Messenger functions | [Chat-over-Email](https://github.com/chatmail/core/blob/main/spec.md#chat-mail-specification) Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][]) User and chat colors | [XEP-0392][]: Consistent Color Generation Send and receive system messages | Multipart/Report Media Type ([RFC 6522][]) diff --git a/test-data/golden/chat_test_parallel_member_remove b/test-data/golden/chat_test_parallel_member_remove index 86e0998578..b548b3e006 100644 --- a/test-data/golden/chat_test_parallel_member_remove +++ b/test-data/golden/chat_test_parallel_member_remove @@ -1,7 +1,7 @@ Group#Chat#10: Group chat [3 member(s)] -------------------------------------------------------------------------------- -Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH] -Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] √ -Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO] -Msg#13: (Contact#Contact#10): What a silence! [FRESH] +Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH] +Msg#11🔒: Me (Contact#Contact#Self): You left the group. [INFO] √ +Msg#12🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO] +Msg#13🔒: (Contact#Contact#10): What a silence! [FRESH] -------------------------------------------------------------------------------- diff --git a/test-data/golden/receive_imf_delayed_removal_is_ignored b/test-data/golden/receive_imf_delayed_removal_is_ignored index 697d5086fe..43c918f7f2 100644 --- a/test-data/golden/receive_imf_delayed_removal_is_ignored +++ b/test-data/golden/receive_imf_delayed_removal_is_ignored @@ -1,9 +1,9 @@ Group#Chat#10: Group [5 member(s)] -------------------------------------------------------------------------------- -Msg#10: Me (Contact#Contact#Self): populate √ -Msg#11: info (Contact#Contact#Info): Member blue@example.net added. [NOTICED][INFO] -Msg#12: info (Contact#Contact#Info): Member fiona (fiona@example.net) removed. [NOTICED][INFO] -Msg#13: bob (Contact#Contact#11): Member orange@example.net added by bob (bob@example.net). [FRESH][INFO] -Msg#14: Me (Contact#Contact#Self): You added member fiona (fiona@example.net). [INFO] o -Msg#15: bob (Contact#Contact#11): Member fiona (fiona@example.net) removed by bob (bob@example.net). [FRESH][INFO] +Msg#10🔒: Me (Contact#Contact#Self): populate √ +Msg#11: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO] +Msg#12: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO] +Msg#13🔒: (Contact#Contact#10): Member elena@example.net added by bob@example.net. [FRESH][INFO] +Msg#14🔒: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o +Msg#15🔒: (Contact#Contact#10): Member fiona@example.net removed by bob@example.net. [FRESH][INFO] -------------------------------------------------------------------------------- diff --git a/test-data/golden/two_group_securejoins b/test-data/golden/two_group_securejoins index 5f66198ef2..f93d0bbe6c 100644 --- a/test-data/golden/two_group_securejoins +++ b/test-data/golden/two_group_securejoins @@ -5,5 +5,5 @@ Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this Waiting for the device of alice@example.org to reply… [NOTICED][INFO] Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] -Msg#18🔒: (Contact#Contact#10): Member Me (fiona@example.net) added by alice@example.org. [FRESH][INFO] +Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO] -------------------------------------------------------------------------------- diff --git a/test-data/key/alice-public.asc b/test-data/key/alice-public.asc deleted file mode 100644 index 3bfa2e0eeb..0000000000 --- a/test-data/key/alice-public.asc +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcB -KAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossj -tTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA -AAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pM -APkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQB -l1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQY -FggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRD -Lo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyM -h2h6cSn/ATn5QJb35pgo+ivp3jsMAg== -=t/Qq ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/key/bob-public.asc b/test-data/key/bob-public.asc deleted file mode 100644 index 84d7df775a..0000000000 --- a/test-data/key/bob-public.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA -5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOe -Jw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeT -xc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+ -x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJ -R2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5u -ZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn2 -4RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEF -LFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2c -nZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbj -mGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZn -DztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C -/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3O -wE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhN -JE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2Z -YK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3d -MA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp -1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6Unx -XGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIb -DBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXR -AJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPB -lqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6 -nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+q -vKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAK -soeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGl -h7oJqgCjZuk= -=14Cq ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/key/charlie-public.asc b/test-data/key/charlie-public.asc deleted file mode 100644 index 56d4ac6b0e..0000000000 --- a/test-data/key/charlie-public.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3 -+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZs -Z2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSV -vvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1 -yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3B -Aj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1w -bGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRm -PL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnW -NNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9 -d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/i -PksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0M -dac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+ -Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOE -JIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJ -HYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY -4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8 -gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+ -DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrO -SME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48 -neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrX -pr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2 -IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaEr -GpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbme -keX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIo -n9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8Ps -bN2P0WCh1CjIJph9 -=WssU ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/key/dom-public.asc b/test-data/key/dom-public.asc deleted file mode 100644 index fa0179767b..0000000000 --- a/test-data/key/dom-public.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3K -vC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34Zx -xiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU -+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isb -D4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/ML -kquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5u -ZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFd -jIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9 -MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMma -s9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b -7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/j -US6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/ -Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzO -wE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv -0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2Jqixx -HfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyf -GR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpf -R6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn -/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIb -DBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ -4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzu -kxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvs -Y8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRG -LIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++ -aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xk -sQeXK7hQrVw= -=eIjV ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/key/elena-public.asc b/test-data/key/elena-public.asc deleted file mode 100644 index da34f7480e..0000000000 --- a/test-data/key/elena-public.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I -7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJ -slu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXc -iOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5Vwfp -vH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFG -fTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxl -Lm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWG -tt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQig -QvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/V -yFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adG -pA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTt -bWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONu -b01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvY -Is7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW -9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1L -P+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayv -z6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXg -LfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHj -Y9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8u -AhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv -0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTl -Jfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdg -vXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqY -LTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq -2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgz -h1vIOV7AOoQ+Rw== -=THka ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/key/fiona-public.asc b/test-data/key/fiona-public.asc deleted file mode 100644 index 115977762e..0000000000 --- a/test-data/key/fiona-public.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2G -JzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9 -431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/ -0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosia -Xlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2F -p8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxl -Lm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQ -v0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3D -vMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPI -c/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl -3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZ -TbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaW -TgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/H -zM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWN -Joq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhD -H4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQy -ttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpf -fUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMn -pf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98 -AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCd -s3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC -/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo -6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3Bd -VCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOo -mIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4 -sjYdVzODl8T0zQ== -=2jSq ------END PGP PUBLIC KEY BLOCK----- diff --git a/test-data/message/blockquote-tag.eml b/test-data/message/blockquote-tag.eml deleted file mode 100644 index 6943ffcb48..0000000000 --- a/test-data/message/blockquote-tag.eml +++ /dev/null @@ -1,47 +0,0 @@ -Return-Path: -User-Agent: K-9 Mail for Android -In-Reply-To: -MIME-Version: 1.0 -Content-Type: multipart/alternative; boundary="----MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0" -Content-Transfer-Encoding: 7bit -Subject: Re: Test -To: Alice -From: Bob -Message-ID: - -------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0 -Content-Type: text/definitelynotplainthiswouldbetooeasy; - charset=utf-8 -Content-Transfer-Encoding: quoted-printable - -Hi Alice, - -some text. - -Am 21=2E Juni 2020 10:38:44 MESZ schrieb Alice : ->Dear Bob, -> ->let's meet -> ->Alice - ---=20 -Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet= -=2E -------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0 -Content-Type: text/html; - charset=utf-8 -Content-Transfer-Encoding: quoted-printable - -Hi Alice,

some text.

-
Am 21=2E Juni 2020 10:38:44 M= -ESZ schrieb Alice <jonathanschmiederer@gmx=2Ede>: -
Sehr geehrte/r Frau/Herr Brenner,

ich habe in= - meinen JuFo-Unterlagen den angeh=C3=A4ngten Gutschein gefunden=2E
Ist e= -s noch m=C3=B6glich, diesen einzul=C3=B6sen?

Mit freundlichen Gr=C3= -=BC=C3=9Fen
Alice

--
= -Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet= -=2E -------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0-- diff --git a/test-data/message/mail_with_user_and_group_avatars.eml b/test-data/message/mail_with_user_and_group_avatars.eml index 2812bdd61b..10ead83444 100644 --- a/test-data/message/mail_with_user_and_group_avatars.eml +++ b/test-data/message/mail_with_user_and_group_avatars.eml @@ -1,7 +1,5 @@ Chat-Group-ID: WVnDtF5azch Chat-Group-Name: =?utf-8?q?testgr1?= -Chat-Group-Avatar: group-image.png -Chat-User-Avatar: avatar.png Subject: =?utf-8?q?Chat=3A_testgr1=3A_hi!_?= Date: Thu, 12 Dec 2019 17:24:03 +0000 X-Mailer: Delta Chat Core 1.0.0-beta.15/CLI @@ -14,6 +12,8 @@ Content-Type: multipart/mixed; boundary="LV8nfXkpyyn39fsVyoB1b29PKDMeb5" --LV8nfXkpyyn39fsVyoB1b29PKDMeb5 Content-Type: text/plain; charset=utf-8 +Chat-Group-Avatar: group-image.png +Chat-User-Avatar: avatar.png hi! diff --git a/test-data/message/mail_with_user_avatar.eml b/test-data/message/mail_with_user_avatar.eml index f03f422392..d19332f2fe 100644 --- a/test-data/message/mail_with_user_avatar.eml +++ b/test-data/message/mail_with_user_avatar.eml @@ -1,4 +1,3 @@ -Chat-User-Avatar: avatar.png Subject: =?utf-8?q?Chat=3A_this_is_a_message_with_a_=2E=2E=2E?= Message-ID: Mr.wOBwZNbBTVt.NZpmQDwWoNk@example.org In-Reply-To: Mr.ETXqza5-WpB.zDEYOLECxAw@example.org @@ -12,6 +11,7 @@ Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw" --luTiGu6GBoVLCvTkzVtmZmwsmhkNMw Content-Type: text/plain; charset=utf-8 +Chat-User-Avatar: avatar.png this is a message with a profile-image attached diff --git a/test-data/message/mail_with_user_avatar_deleted.eml b/test-data/message/mail_with_user_avatar_deleted.eml index 37550901eb..c9af9a7272 100644 --- a/test-data/message/mail_with_user_avatar_deleted.eml +++ b/test-data/message/mail_with_user_avatar_deleted.eml @@ -1,5 +1,3 @@ -Content-Type: text/plain; charset=utf-8 -Chat-User-Avatar: 0 Subject: =?utf-8?q?Chat=3A_profile_image_deleted?= Message-ID: Mr.tsgoJgn-cBf.0TkFWKJzeSp@example.org Date: Sun, 08 Dec 2019 23:28:30 +0000 @@ -7,8 +5,16 @@ X-Mailer: Delta Chat Core 1.0.0-beta.12/CLI Chat-Version: 1.0 To: From: "=?utf-8?q??=" +Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw" + + +--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw +Content-Type: text/plain; charset=utf-8 +Chat-User-Avatar: 0 profile image deleted -- Sent with my Delta Chat Messenger: https://delta.chat + +--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw-- diff --git a/test-data/message/mailparse-0.16.0-panic.eml b/test-data/message/mailparse-0.16.0-panic.eml new file mode 100644 index 0000000000..2ea7f95d2f --- /dev/null +++ b/test-data/message/mailparse-0.16.0-panic.eml @@ -0,0 +1,3 @@ +Content-Type: multipart/mixed; boundary="foobar" + +--foobar--