mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
53 Commits
v1.155.2
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b0ad62a4d | ||
|
|
4f71c77ae4 | ||
|
|
96704eb73d | ||
|
|
5c3d1e7dae | ||
|
|
4fb24d05dc | ||
|
|
9b6ef5e54f | ||
|
|
81e9628ab7 | ||
|
|
aaa02968d3 | ||
|
|
302aa5a5f7 | ||
|
|
8bddd455a7 | ||
|
|
a0ff0d71bc | ||
|
|
068726453e | ||
|
|
0973a46245 | ||
|
|
e22d980845 | ||
|
|
0c0afead2c | ||
|
|
3eae9cb30c | ||
|
|
4ef6788ffd | ||
|
|
4198ed1efb | ||
|
|
6f5620dad5 | ||
|
|
1d55458781 | ||
|
|
6297bb967a | ||
|
|
0040c17892 | ||
|
|
258b5cde70 | ||
|
|
a58103ae4a | ||
|
|
bf36a479db | ||
|
|
9a2924ed88 | ||
|
|
4be4a3c72f | ||
|
|
7b6ba0e011 | ||
|
|
4e601c31b4 | ||
|
|
fa0382da2d | ||
|
|
64bd05aa44 | ||
|
|
e651001a57 | ||
|
|
8c251afeb1 | ||
|
|
8e7f1d83ec | ||
|
|
15fc12e525 | ||
|
|
81930c1731 | ||
|
|
ee39615dbd | ||
|
|
058ac3006c | ||
|
|
f0c4414d34 | ||
|
|
4e5125b98d | ||
|
|
8cb1ba5000 | ||
|
|
feac84c5fc | ||
|
|
d762972c95 | ||
|
|
ae893d57a9 | ||
|
|
602d379aef | ||
|
|
18c02f5bf9 | ||
|
|
23033fb0a0 | ||
|
|
5e65c19f00 | ||
|
|
c23809ccd5 | ||
|
|
54d3a2ad47 | ||
|
|
1f7e57181e | ||
|
|
7e886cbf2b | ||
|
|
ebeb742ba6 |
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -16,6 +16,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
@@ -24,11 +26,12 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.84.0
|
||||
RUSTUP_TOOLCHAIN: 1.84.1
|
||||
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
|
||||
- name: Cache rust cargo artifacts
|
||||
@@ -42,18 +45,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 +52,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 +66,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 +79,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,11 +91,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.84.1
|
||||
- os: windows-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.84.1
|
||||
- os: macos-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.84.1
|
||||
|
||||
# Minimum Supported Rust Version = 1.81.0
|
||||
- os: ubuntu-latest
|
||||
@@ -111,6 +105,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||
@@ -147,6 +142,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 +167,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 +189,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -234,6 +232,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -286,6 +285,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
14
.github/workflows/deltachat-rpc-server.yml
vendored
14
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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,6 +33,7 @@ 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
|
||||
|
||||
@@ -55,6 +58,7 @@ 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
|
||||
|
||||
@@ -80,6 +84,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,6 +110,7 @@ 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
|
||||
|
||||
@@ -132,6 +138,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pack-module:
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
@@ -15,6 +17,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
3
.github/workflows/jsonrpc.yml
vendored
3
.github/workflows/jsonrpc.yml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/nix.yml
vendored
5
.github/workflows/nix.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: check flake formatting
|
||||
@@ -20,6 +22,7 @@ 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
|
||||
@@ -80,6 +83,7 @@ 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 }}
|
||||
@@ -99,6 +103,7 @@ 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 }}
|
||||
|
||||
41
.github/workflows/node-docs.yml
vendored
41
.github/workflows/node-docs.yml
vendored
@@ -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 <https://js.delta.chat/>
|
||||
|
||||
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/"
|
||||
235
.github/workflows/node-package.yml
vendored
235
.github/workflows/node-package.yml
vendored
@@ -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/"
|
||||
68
.github/workflows/node-tests.yml
vendored
68
.github/workflows/node-tests.yml
vendored
@@ -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"
|
||||
@@ -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
|
||||
|
||||
3
.github/workflows/repl.yml
vendored
3
.github/workflows/repl.yml
vendored
@@ -7,6 +7,8 @@ name: Build Windows REPL .exe
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build_repl:
|
||||
name: Build REPL example
|
||||
@@ -15,6 +17,7 @@ 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
|
||||
|
||||
6
.github/workflows/upload-docs.yml
vendored
6
.github/workflows/upload-docs.yml
vendored
@@ -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,6 +34,7 @@ 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
|
||||
@@ -50,6 +54,7 @@ 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
|
||||
@@ -72,6 +77,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
|
||||
|
||||
3
.github/workflows/upload-ffi-docs.yml
vendored
3
.github/workflows/upload-ffi-docs.yml
vendored
@@ -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
|
||||
|
||||
31
.github/workflows/zizmor-scan.yml
vendored
Normal file
31
.github/workflows/zizmor-scan.yml
vendored
Normal file
@@ -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@v5
|
||||
|
||||
- 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
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,7 +1,81 @@
|
||||
# Changelog
|
||||
|
||||
## [1.155.5] - 2025-02-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Get_filename() is now guaranteed to return a valid filename ([#6537](https://github.com/deltachat/deltachat-core-rust/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/deltachat/deltachat-core-rust/pull/6528)).
|
||||
|
||||
### Other
|
||||
|
||||
- Add `MessageQuote.chat_id`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move even more tests into their own files ([#6521](https://github.com/deltachat/deltachat-core-rust/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)).
|
||||
@@ -5725,3 +5799,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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
|
||||
[1.155.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.2..v1.155.3
|
||||
[1.155.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.3..v1.155.4
|
||||
[1.155.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.4..v1.155.5
|
||||
|
||||
746
Cargo.lock
generated
746
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
@@ -49,7 +49,7 @@ base64 = { workspace = true }
|
||||
brotli = { version = "7", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
data-encoding = "2.6.0"
|
||||
data-encoding = "2.7.0"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = "0.2"
|
||||
escaper = "0.1"
|
||||
@@ -64,12 +64,12 @@ 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 }
|
||||
iroh-gossip = { version = "0.32", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.32", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
mailparse = "0.16"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
@@ -77,7 +77,7 @@ 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,8 +86,8 @@ 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"
|
||||
@@ -110,7 +110,7 @@ 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"
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.5.5"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -176,7 +176,7 @@ 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"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -189,7 +189,7 @@ sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.14.0"
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.13"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
@@ -176,9 +176,7 @@ To add the updates from the
|
||||
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/)\]
|
||||
|
||||
@@ -78,21 +78,21 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
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"
|
||||
"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}\n");
|
||||
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}\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\n");
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
}
|
||||
res += "END:VCARD\n";
|
||||
res += "END:VCARD\r\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -206,22 +206,21 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
} else if let Some(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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -540,6 +539,30 @@ END:VCARD",
|
||||
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 = [
|
||||
@@ -559,20 +582,20 @@ END:VCARD",
|
||||
},
|
||||
];
|
||||
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",
|
||||
"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() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -4930,6 +4930,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 +4945,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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -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<String>,
|
||||
},
|
||||
|
||||
/// 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,29 @@ 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,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged { contact_id: Option<u32> },
|
||||
ContactsChanged {
|
||||
/// If set, this is the contact_id of an added contact that should be selected.
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// 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<u32> },
|
||||
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<u32>,
|
||||
},
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
@@ -195,10 +272,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 +293,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 +341,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<u8> },
|
||||
WebxdcRealtimeData {
|
||||
/// Message ID.
|
||||
msg_id: u32,
|
||||
|
||||
/// Realtime data.
|
||||
data: Vec<u8>,
|
||||
},
|
||||
|
||||
/// 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 +387,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<u32> },
|
||||
ChatlistItemChanged {
|
||||
/// ID of the changed chat
|
||||
chat_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
||||
///
|
||||
@@ -303,7 +407,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<CoreEventType> for EventType {
|
||||
|
||||
@@ -104,6 +104,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<String>,
|
||||
@@ -147,6 +150,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(),
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.155.2"
|
||||
"version": "1.155.5"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
[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"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.155.2"
|
||||
"version": "1.155.5"
|
||||
}
|
||||
|
||||
14
deny.toml
14
deny.toml
@@ -14,6 +14,9 @@ ignore = [
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# DNSSEC validation that we don't use anyway.
|
||||
"RUSTSEC-2025-0006",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -26,19 +29,13 @@ skip = [
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ 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 = "http", version = "0.2.12" },
|
||||
{ name = "iroh-metrics", version = "0.30.0" },
|
||||
{ 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" },
|
||||
@@ -49,11 +46,14 @@ skip = [
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-tungstenite", version = "0.21.0" },
|
||||
{ name = "tungstenite", version = "0.21.0" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows" },
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# .prettierrc
|
||||
trailingComma: es5
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: true
|
||||
jsxSingleQuote: true
|
||||
@@ -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** | |
|
||||
674
node/LICENSE
674
node/LICENSE
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
260
node/README.md
260
node/README.md
@@ -1,260 +0,0 @@
|
||||
# deltachat-node
|
||||
|
||||
> node.js bindings for [`deltachat-core-rust`](..)
|
||||
|
||||
[](https://www.npmjs.com/package/deltachat-node)
|
||||

|
||||
[](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
|
||||
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
- [Install](#install)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Usage](#usage)
|
||||
- [Developing](#developing)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
## 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 <https://download.delta.chat/node/preview/>.
|
||||
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).
|
||||
|
||||

|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true
|
||||
|
||||
[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8
|
||||
@@ -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%": "<!(echo $USE_SYSTEM_LIBDELTACHAT)",
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "deltachat",
|
||||
"sources": ["./src/module.c"],
|
||||
"include_dirs": ["<!(node -e \"require('napi-macros')\")"],
|
||||
"conditions": [
|
||||
[
|
||||
"OS == 'win'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"libraries": [
|
||||
"../../target/release/deltachat.dll.lib",
|
||||
],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT == 'true'",
|
||||
{
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'linux' or OS == 'mac'",
|
||||
{
|
||||
"libraries": ["-lpthread"],
|
||||
"cflags": ["-std=gnu99"],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT != 'true'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
|
||||
"libraries": [
|
||||
"../../target/release/libdeltachat.a",
|
||||
"-ldl",
|
||||
],
|
||||
"conditions": [],
|
||||
},
|
||||
{
|
||||
# USE_SYSTEM_LIBDELTACHAT == 'true'
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'mac'",
|
||||
{
|
||||
"libraries": [
|
||||
"-framework CoreFoundation",
|
||||
"-framework CoreServices",
|
||||
"-framework Security",
|
||||
"-lresolv",
|
||||
],
|
||||
},
|
||||
{
|
||||
# OS == 'linux'
|
||||
"libraries": ["-lm", "-lrt"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('node-gyp-build')(__dirname)
|
||||
@@ -1,311 +0,0 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
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
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/* eslint-disable quotes */
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
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'
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
//@ts-check
|
||||
const { Context } = require('../dist')
|
||||
|
||||
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()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -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
|
||||
111
node/lib/chat.ts
111
node/lib/chat.ts
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<string> {
|
||||
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<number>} Promise that resolves into the resulting message id
|
||||
*/
|
||||
sendVideochatInvitation(chatId: number): Promise<number> {
|
||||
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<T>(
|
||||
msgId: number,
|
||||
json: WebxdcSendingStatusUpdate<T>,
|
||||
descr: string
|
||||
) {
|
||||
return Boolean(
|
||||
binding.dcn_send_webxdc_status_update(
|
||||
this.dcn_context,
|
||||
msgId,
|
||||
JSON.stringify(json),
|
||||
descr
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getWebxdcStatusUpdates<T>(
|
||||
msgId: number,
|
||||
serial = 0
|
||||
): WebxdcReceivedStatusUpdate<T>[] {
|
||||
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<T> = {
|
||||
/** 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<T> = {
|
||||
/** 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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<Locations['locationToJson']>[] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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": [],
|
||||
@@ -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 }
|
||||
@@ -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`
|
||||
)
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
3600
node/src/module.c
3600
node/src/module.c
File diff suppressed because it is too large
Load Diff
@@ -1,144 +0,0 @@
|
||||
#include <napi-macros.h>
|
||||
|
||||
#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])
|
||||
BIN
node/test/fixtures/avatar.png
vendored
BIN
node/test/fixtures/avatar.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
BIN
node/test/fixtures/image.jpeg
vendored
BIN
node/test/fixtures/image.jpeg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
BIN
node/test/fixtures/logo.png
vendored
BIN
node/test/fixtures/logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@@ -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<any> }}
|
||||
*/
|
||||
function waitForSomething() {
|
||||
let resolvePromise
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolvePromise = res
|
||||
})
|
||||
return {
|
||||
done: resolvePromise,
|
||||
promise,
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
> Steps on how to get windows set up properly for the node bindings
|
||||
|
||||
## install git
|
||||
|
||||
E.g via <https://git-scm.com/download/win>
|
||||
|
||||
## install node
|
||||
|
||||
Download and install `v18` from <https://nodejs.org/en/>
|
||||
|
||||
## install rust
|
||||
|
||||
Download and run `rust-init.exe` from <https://www.rust-lang.org/tools/install>
|
||||
|
||||
## 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
|
||||
```
|
||||
59
package.json
59
package.json
@@ -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"
|
||||
}
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.155.2"
|
||||
version = "1.155.5"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-01-31
|
||||
2025-02-14
|
||||
@@ -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.84.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
864
src/blob.rs
864
src/blob.rs
@@ -23,6 +23,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.
|
||||
///
|
||||
@@ -92,7 +93,7 @@ impl<'a> BlobObject<'a> {
|
||||
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 (stem, ext) = BlobObject::sanitize_name_and_split_extension(&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();
|
||||
@@ -159,10 +160,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}")
|
||||
};
|
||||
@@ -215,7 +215,7 @@ impl<'a> BlobObject<'a> {
|
||||
/// 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
|
||||
/// subdirectory is used and [BlobObject::sanitize_name_and_split_extension] does not
|
||||
/// modify the filename.
|
||||
///
|
||||
/// Paths into the blob directory may be either defined by an absolute path
|
||||
@@ -311,41 +311,14 @@ 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
|
||||
/// The 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);
|
||||
fn sanitize_name_and_split_extension(name: &str) -> (String, String) {
|
||||
let name = sanitize_filename(name);
|
||||
// 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.
|
||||
@@ -844,821 +817,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<Path>, 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<DynamicImage> {
|
||||
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;
|
||||
|
||||
813
src/blob/blob_tests.rs
Normal file
813
src/blob/blob_tests.rs
Normal file
@@ -0,0 +1,813 @@
|
||||
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<Path>, 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::sanitize_name_and_split_extension(
|
||||
"Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.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::sanitize_name_and_split_extension("wot.tar.gz");
|
||||
assert_eq!(stem, "wot");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitize_name_and_split_extension(".foo.bar");
|
||||
assert_eq!(stem, "file");
|
||||
assert_eq!(ext, ".foo.bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains('?'));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("no-extension");
|
||||
assert_eq!(stem, "no-extension");
|
||||
assert_eq!(ext, "");
|
||||
|
||||
let (stem, ext) =
|
||||
BlobObject::sanitize_name_and_split_extension("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::sanitize_name_and_split_extension(
|
||||
"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::sanitize_name_and_split_extension("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("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<DynamicImage> {
|
||||
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(())
|
||||
}
|
||||
@@ -455,6 +455,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 {
|
||||
|
||||
651
src/context.rs
651
src/context.rs
@@ -1480,653 +1480,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 <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\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;
|
||||
|
||||
649
src/context/context_tests.rs
Normal file
649
src/context/context_tests.rs
Normal file
@@ -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 <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\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(())
|
||||
}
|
||||
806
src/ephemeral.rs
806
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<String> = 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 <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\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 <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\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 <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. 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 <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\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;
|
||||
|
||||
781
src/ephemeral/ephemeral_tests.rs
Normal file
781
src/ephemeral/ephemeral_tests.rs
Normal file
@@ -0,0 +1,781 @@
|
||||
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<String> = 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 <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\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 <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\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 <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. 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 <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\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(())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -371,7 +371,6 @@ and will be wrapped as usual.<br/>
|
||||
mime-modified should not be set set as there is no html and no special stuff;<br/>
|
||||
although not being a delta-message.<br/>
|
||||
test some special html-characters as < > and & but also " and ' :)<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -405,7 +404,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
r##"<html>
|
||||
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
@@ -422,7 +420,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
this is <b>html</b>
|
||||
</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
|
||||
407
src/imap.rs
407
src/imap.rs
@@ -1589,15 +1589,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?;
|
||||
|
||||
// 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 folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
let encrypted_device_token = encrypt_device_token(&device_token)
|
||||
.context("Failed to encrypt device token")?;
|
||||
|
||||
@@ -1606,22 +1606,23 @@ impl Session {
|
||||
// <https://www.rfc-editor.org/rfc/rfc7888>.
|
||||
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 +1634,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 +2641,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<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().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: <abc@example.com>\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<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, 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<i64>, 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<i64>, 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;
|
||||
|
||||
@@ -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<Self> {
|
||||
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();
|
||||
|
||||
Ok(Event::Interrupt)
|
||||
});
|
||||
tokio::spawn(async move {
|
||||
idle_interrupt_receiver.recv().await.ok();
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{folder:?}: Received interrupt, stopping IDLE.");
|
||||
|
||||
// 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"))?
|
||||
|
||||
337
src/imap/imap_tests.rs
Normal file
337
src/imap/imap_tests.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
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<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().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: <abc@example.com>\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<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, 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<i64>, 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<i64>, 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)"
|
||||
);
|
||||
}
|
||||
@@ -32,8 +32,8 @@ use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
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, truncate,
|
||||
};
|
||||
|
||||
/// Message ID, including reserved IDs.
|
||||
@@ -807,12 +807,12 @@ impl Message {
|
||||
/// To get the full path, use [`Self::get_file()`].
|
||||
pub fn get_filename(&self) -> Option<String> {
|
||||
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.
|
||||
|
||||
@@ -755,3 +755,31 @@ async fn test_delete_msgs_offline() -> Result<()> {
|
||||
|
||||
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_blob(Param::File, t).await?.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(())
|
||||
}
|
||||
|
||||
@@ -1817,3 +1817,78 @@ async fn test_protect_autocrypt() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 <https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1>
|
||||
// with a `Content-Disposition` headers added to turn files
|
||||
// into attachments.
|
||||
let raw = b"From: Nathaniel Borenstein <nsb@bellcore.com>\r
|
||||
To: Ned Freed <ned@innosoft.com>\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_blob(Param::File, &context)
|
||||
.await
|
||||
.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_blob(Param::File, &context)
|
||||
.await
|
||||
.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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,6 +108,10 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
|
||||
// use the same path for all app versions,
|
||||
// so may change, but it is not critical if outdated icon is displayed.
|
||||
now + 3600 * 24
|
||||
} else if url.ends_with(".json") {
|
||||
// Revalidate JSON such as https://apps.testrun.org/xdcget-lock.json
|
||||
// every 10 minutes.
|
||||
now + 600
|
||||
} else {
|
||||
// Revalidate everything else after 1 hour.
|
||||
//
|
||||
|
||||
@@ -379,13 +379,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();
|
||||
|
||||
919
src/qr.rs
919
src/qr.rs
@@ -261,6 +261,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())
|
||||
}
|
||||
@@ -290,7 +297,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
|
||||
decode_shadowsocks_proxy(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
|
||||
decode_backup2(qr)?
|
||||
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) {
|
||||
@@ -947,911 +955,4 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
921
src/qr/qr_tests.rs
Normal file
921
src/qr/qr_tests.rs
Normal file
@@ -0,0 +1,921 @@
|
||||
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 { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5582,3 +5582,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: <tmp_5890965001269692@testrun.org>
|
||||
From: \"=?utf-8?q??=\" <tmp_6272287793210918@testrun.org>
|
||||
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_blob(Param::File, alice).await?.unwrap();
|
||||
assert_eq!(blob.suffix().unwrap(), "html");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -744,850 +744,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 <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
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;
|
||||
|
||||
841
src/securejoin/securejoin_tests.rs
Normal file
841
src/securejoin/securejoin_tests.rs
Normal file
@@ -0,0 +1,841 @@
|
||||
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 <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
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(())
|
||||
}
|
||||
375
src/sql.rs
375
src/sql.rs
@@ -1044,377 +1044,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:
|
||||
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
||||
#[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<String> = 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<u32> = 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;
|
||||
|
||||
372
src/sql/sql_tests.rs
Normal file
372
src/sql/sql_tests.rs
Normal file
@@ -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:
|
||||
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
||||
#[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<String> = 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<u32> = 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(())
|
||||
}
|
||||
185
src/stock_str.rs
185
src/stock_str.rs
@@ -1439,187 +1439,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;
|
||||
|
||||
182
src/stock_str/stock_str_tests.rs
Normal file
182
src/stock_str/stock_str_tests.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
35
src/tools.rs
35
src/tools.rs
@@ -366,6 +366,41 @@ pub(crate) async fn delete_file(context: &Context, path: impl AsRef<Path>) -> 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`.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
Return-Path: <bob@example.org>
|
||||
User-Agent: K-9 Mail for Android
|
||||
In-Reply-To: <hasnihae@gmx.de>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="----MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Subject: Re: Test
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.org>
|
||||
Message-ID: <haeisnr@example.org>
|
||||
|
||||
------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 <alice@example=2Eorg>:
|
||||
>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
|
||||
|
||||
<html><head></head><body>Hi Alice,<br><br>some text.<br><br>
|
||||
<div class=3D"gmail_quote">Am 21=2E Juni 2020 10:38:44 M=
|
||||
ESZ schrieb Alice <jonathanschmiederer@gmx=2Ede>:<bloc=
|
||||
kquote class=3D"gmail_quote" style=3D"margin: 0pt 0pt 0pt 0=2E8ex; border-l=
|
||||
eft: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
|
||||
<pre class=3D"k9mail">Sehr geehrte/r Frau/Herr Brenner,<br><br>ich habe in=
|
||||
meinen JuFo-Unterlagen den angeh=C3=A4ngten Gutschein gefunden=2E<br>Ist e=
|
||||
s noch m=C3=B6glich, diesen einzul=C3=B6sen?<br><br>Mit freundlichen Gr=C3=
|
||||
=BC=C3=9Fen<br>Alice<br></pre></blockquote></div><br>-- <br>=
|
||||
Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet=
|
||||
=2E</body></html>
|
||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0--
|
||||
Reference in New Issue
Block a user