mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
193 Commits
link2xt/li
...
v1.120.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d74f625d3 | ||
|
|
0a94fbc735 | ||
|
|
9ef34890fa | ||
|
|
3e07f2c173 | ||
|
|
ee28298d7f | ||
|
|
62aed13880 | ||
|
|
bffe934acc | ||
|
|
87ffcaf03e | ||
|
|
2635146328 | ||
|
|
d727d85f6d | ||
|
|
81a7af10c7 | ||
|
|
4a6e94f8ab | ||
|
|
146fe50e20 | ||
|
|
9bf2850fb1 | ||
|
|
ba2c36548e | ||
|
|
d07c743cdc | ||
|
|
d70c1d48b5 | ||
|
|
a8e0cb9b5a | ||
|
|
6ea9a8988b | ||
|
|
45e35b3571 | ||
|
|
e43f9066d8 | ||
|
|
bba6c8f15a | ||
|
|
55aaec744a | ||
|
|
2f24eddb7d | ||
|
|
a33c91afa9 | ||
|
|
d52f2883cf | ||
|
|
b872953bc5 | ||
|
|
c1cb6eef08 | ||
|
|
200b808c27 | ||
|
|
d572d960e5 | ||
|
|
5db75128ba | ||
|
|
fbd2fc8ead | ||
|
|
bc73c16df7 | ||
|
|
0a50bad555 | ||
|
|
82c0058129 | ||
|
|
1bd307a26a | ||
|
|
740f43a2d6 | ||
|
|
c14f45a8f5 | ||
|
|
8269116dba | ||
|
|
db941ccf88 | ||
|
|
a464cbdfe6 | ||
|
|
ea4a0530b8 | ||
|
|
9d3b2d4844 | ||
|
|
c312280ab3 | ||
|
|
572b99a2e1 | ||
|
|
3992b5a063 | ||
|
|
b97cb4b55e | ||
|
|
64c218f1ea | ||
|
|
deed790950 | ||
|
|
b33ae3cd0f | ||
|
|
9480699362 | ||
|
|
94c190e844 | ||
|
|
578e47666f | ||
|
|
7eeced50d1 | ||
|
|
46e127ad27 | ||
|
|
4891849e28 | ||
|
|
e0dd83d538 | ||
|
|
aac8bb950c | ||
|
|
bf21796bc0 | ||
|
|
9cbf413064 | ||
|
|
1b57eb4d8d | ||
|
|
5152e702bd | ||
|
|
c80f1a1997 | ||
|
|
88759c815b | ||
|
|
9c68fac4b6 | ||
|
|
8e17e400b3 | ||
|
|
dae3857db8 | ||
|
|
695f71e124 | ||
|
|
2d30afd212 | ||
|
|
5fe94e8bce | ||
|
|
1351f71632 | ||
|
|
d42322b38b | ||
|
|
ce6876c418 | ||
|
|
2a6b7d9766 | ||
|
|
fa1924da2b | ||
|
|
d5214eb192 | ||
|
|
c47324d671 | ||
|
|
3f8ec5ec56 | ||
|
|
fab504b54c | ||
|
|
dd32430ade | ||
|
|
eb943625a6 | ||
|
|
32ac4a01ca | ||
|
|
f01a9d7d5c | ||
|
|
a5db7104c2 | ||
|
|
18aeb14003 | ||
|
|
4ad2d6e340 | ||
|
|
ce9cd54993 | ||
|
|
23f540f9f9 | ||
|
|
f994b2d8e4 | ||
|
|
6e42b85a36 | ||
|
|
d69e42377d | ||
|
|
de9330b52f | ||
|
|
01d1c4c04b | ||
|
|
7d98978269 | ||
|
|
5024f48609 | ||
|
|
e975568122 | ||
|
|
1f71c69325 | ||
|
|
b80ec8507c | ||
|
|
3a3f3542d9 | ||
|
|
657c5fa947 | ||
|
|
7d0b25c209 | ||
|
|
8d26303cad | ||
|
|
0d8a76593a | ||
|
|
7b49fb2eb6 | ||
|
|
efa37dd283 | ||
|
|
323e44da04 | ||
|
|
70efd0f10a | ||
|
|
fcec81b4c1 | ||
|
|
dd806b2d88 | ||
|
|
5659c1b9c2 | ||
|
|
d538d29b94 | ||
|
|
b4209fac2e | ||
|
|
4d6dfa120e | ||
|
|
f92108be1d | ||
|
|
00cb72f04d | ||
|
|
92e34d67e6 | ||
|
|
65bff8339f | ||
|
|
768f8175e6 | ||
|
|
c3f352aff1 | ||
|
|
5ac2d1b8cb | ||
|
|
8214b2b8c1 | ||
|
|
53ab8a3b35 | ||
|
|
cbe1671104 | ||
|
|
0d0e223238 | ||
|
|
4767f1ce74 | ||
|
|
1a62b6d77f | ||
|
|
915008d474 | ||
|
|
9646766793 | ||
|
|
e948ec3256 | ||
|
|
9ab9d2eb7b | ||
|
|
437f8c48c4 | ||
|
|
e6d9a49187 | ||
|
|
33a014eea4 | ||
|
|
9be871ccf6 | ||
|
|
6eb8abe535 | ||
|
|
91bf87fa80 | ||
|
|
a2599ef08a | ||
|
|
22d0a4bb32 | ||
|
|
7a160033b6 | ||
|
|
3442748be7 | ||
|
|
d451bcfbe3 | ||
|
|
b2993242e4 | ||
|
|
5eaa9eeed2 | ||
|
|
3ed2ac8f0c | ||
|
|
0145203f7b | ||
|
|
59588b319e | ||
|
|
f917c7de6b | ||
|
|
84888fa4c4 | ||
|
|
e0b1644488 | ||
|
|
4beba8ce3c | ||
|
|
bc521a685d | ||
|
|
33caa0f499 | ||
|
|
033ce41c0f | ||
|
|
88a62e1f6e | ||
|
|
dd30f6ab7d | ||
|
|
140d116d98 | ||
|
|
d96b783909 | ||
|
|
572c7f2efb | ||
|
|
bcd6c226f6 | ||
|
|
bae61746f8 | ||
|
|
31f2766074 | ||
|
|
b06c8baa9c | ||
|
|
1e479fe4a3 | ||
|
|
8ea8ee02ed | ||
|
|
55bc556bcf | ||
|
|
3b6d21301b | ||
|
|
472195c7d9 | ||
|
|
afb8b5ce55 | ||
|
|
de3c82ef43 | ||
|
|
4255ae4c2d | ||
|
|
4b4e2f700e | ||
|
|
81fde5c680 | ||
|
|
5340a7d033 | ||
|
|
fc82d728fc | ||
|
|
136e9179e9 | ||
|
|
31e19ca56c | ||
|
|
f2b02b7bb0 | ||
|
|
646ace8e7a | ||
|
|
a2495716b6 | ||
|
|
0f579c6415 | ||
|
|
3eddc9164c | ||
|
|
dd29fae49b | ||
|
|
5b435d11c7 | ||
|
|
b9b0d20e8d | ||
|
|
c68a2e3820 | ||
|
|
c7ad0b1f4f | ||
|
|
0dd9e3a77e | ||
|
|
d27e3d085e | ||
|
|
10b2aa5350 | ||
|
|
3a29a555bf | ||
|
|
f024f396bf | ||
|
|
24d52c5909 | ||
|
|
f9dc8edbcb |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,6 +2,14 @@
|
||||
# ensures this even if the user has not set core.autocrlf.
|
||||
* text=auto
|
||||
|
||||
# Checkout JavaScript files with LF line endings
|
||||
# to prevent `prettier` from reporting errors on Windows.
|
||||
*.js eol=lf
|
||||
*.jsx eol=lf
|
||||
*.ts eol=lf
|
||||
*.tsx eol=lf
|
||||
*.json eol=lf
|
||||
|
||||
# This directory contains email messages verbatim, and changing CRLF to
|
||||
# LF will corrupt them.
|
||||
test-data/** text=false
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -5,5 +5,5 @@ updates:
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "cargo"
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
|
||||
15
.github/mergeable.yml
vendored
15
.github/mergeable.yml
vendored
@@ -1,15 +0,0 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Conventional Commits"
|
||||
validate:
|
||||
- do: title
|
||||
begins_with:
|
||||
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore', 'cargo', 'build', 'ci']
|
||||
|
||||
fail:
|
||||
- do: checks
|
||||
status: "action_required"
|
||||
payload:
|
||||
title: PR title should follow conventional commits
|
||||
summary: "PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' (for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)"
|
||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -1,3 +1,7 @@
|
||||
# GitHub Actions workflow to
|
||||
# lint Rust and Python code
|
||||
# and run Rust tests, Python tests and async Python tests.
|
||||
|
||||
name: Rust CI
|
||||
|
||||
# Cancel previously started workflow runs
|
||||
@@ -11,6 +15,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
@@ -20,7 +25,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.68.2
|
||||
RUSTUP_TOOLCHAIN: 1.72.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -34,6 +39,10 @@ jobs:
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
|
||||
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
|
||||
- name: Check musl
|
||||
run: scripts/zig-musl-check.sh
|
||||
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
@@ -222,7 +231,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
- os: macos-latest
|
||||
@@ -234,11 +242,12 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
# Minimum Supported Python Version = 3.8
|
||||
#
|
||||
# Python 3.7 has at least one known bug related to starting subprocesses
|
||||
# in asyncio programs: <https://bugs.python.org/issue35621>
|
||||
- os: ubuntu-latest
|
||||
python: 3.7
|
||||
python: 3.8
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
8
.github/workflows/deltachat-rpc-server.yml
vendored
8
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -1,4 +1,10 @@
|
||||
# Manually triggered action to build deltachat-rpc-server binaries.
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat-rpc-server` binaries
|
||||
# and upload them to the release.
|
||||
#
|
||||
# The workflow is automatically triggered on releases.
|
||||
# It can also be triggered manually
|
||||
# to produce binary artifacts for testing.
|
||||
|
||||
name: Build deltachat-rpc-server binaries
|
||||
|
||||
|
||||
3
.github/workflows/dependabot.yml
vendored
3
.github/workflows/dependabot.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# GitHub Actions workflow
|
||||
# to automatically approve PRs made by Dependabot.
|
||||
|
||||
name: Dependabot auto-approve
|
||||
on: pull_request
|
||||
|
||||
|
||||
@@ -38,13 +38,12 @@ jobs:
|
||||
node --version
|
||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install --ignore-scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
- name: Package
|
||||
shell: bash
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build
|
||||
npm pack .
|
||||
ls -lah
|
||||
|
||||
25
.github/workflows/jsonrpc.yml
vendored
25
.github/workflows/jsonrpc.yml
vendored
@@ -22,24 +22,19 @@ jobs:
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: npm install
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run build
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run test
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: make sure websocket server version still builds
|
||||
run: |
|
||||
cd deltachat-jsonrpc
|
||||
cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||
working-directory: deltachat-jsonrpc
|
||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||
- name: Run linter
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run prettier:check
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
31
.github/workflows/node-delete-preview.yml
vendored
31
.github/workflows/node-delete-preview.yml
vendored
@@ -1,31 +0,0 @@
|
||||
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
|
||||
name: Delete node PR previews
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get Pull Request ID
|
||||
id: getid
|
||||
run: |
|
||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
||||
- name: Renaming
|
||||
run: |
|
||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
||||
echo "This preview build is outdated and has been removed." > empty
|
||||
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
|
||||
- name: Replace builds with dummy files
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
host: "download.delta.chat"
|
||||
port: 22
|
||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
||||
remote: "/var/www/html/download/node/preview/"
|
||||
7
.github/workflows/node-docs.yml
vendored
7
.github/workflows/node-docs.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# 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:
|
||||
@@ -17,8 +22,8 @@ jobs:
|
||||
node-version: 16.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
working-directory: node
|
||||
run: |
|
||||
cd node
|
||||
npm i --ignore-scripts
|
||||
npx typedoc
|
||||
mv docs js
|
||||
|
||||
90
.github/workflows/node-package.yml
vendored
90
.github/workflows/node-package.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
os: [macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -46,13 +46,12 @@ jobs:
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
working-directory: node
|
||||
run: npm install --verbose
|
||||
|
||||
- name: Build Prebuild
|
||||
working-directory: node
|
||||
run: |
|
||||
cd node
|
||||
npm run prebuildify
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
@@ -62,10 +61,79 @@ jobs:
|
||||
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 at the time of the writing (2023-06-04): 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 .
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
- 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@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
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@v3
|
||||
with:
|
||||
name: linux
|
||||
path: node/linux.tar.gz
|
||||
|
||||
pack-module:
|
||||
needs: prebuild
|
||||
needs: [prebuild, prebuild-linux]
|
||||
name: Package deltachat-node and upload to download.delta.chat
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
@@ -96,10 +164,10 @@ jobs:
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download Ubuntu prebuild
|
||||
- name: Download Linux prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: ubuntu-20.04
|
||||
name: linux
|
||||
- name: Download macOS prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
@@ -111,11 +179,11 @@ jobs:
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
|
||||
tar -xvzf linux/linux.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
rm -rf ubuntu-20.04 macos-latest windows-latest
|
||||
rm -rf linux macos-latest windows-latest
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
|
||||
23
.github/workflows/node-tests.yml
vendored
23
.github/workflows/node-tests.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# GitHub Actions workflow
|
||||
# to test Node.js bindings.
|
||||
|
||||
name: "node.js tests"
|
||||
|
||||
# Cancel previously started workflow runs
|
||||
@@ -52,25 +55,13 @@ jobs:
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
working-directory: node
|
||||
run: npm install --verbose
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 10
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||
- name: Run tests on Windows, except lint
|
||||
timeout-minutes: 10
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test:mocha
|
||||
working-directory: node
|
||||
run: npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||
|
||||
3
.github/workflows/repl.yml
vendored
3
.github/workflows/repl.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# Manually triggered action to build a Windows repl.exe which users can
|
||||
# Manually triggered GitHub Actions workflow
|
||||
# to build a Windows repl.exe which users can
|
||||
# download to debug complex bugs.
|
||||
|
||||
name: Build Windows REPL .exe
|
||||
|
||||
4
.github/workflows/upload-ffi-docs.yml
vendored
4
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,3 +1,7 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_fii` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
|
||||
275
CHANGELOG.md
275
CHANGELOG.md
@@ -1,5 +1,273 @@
|
||||
# Changelog
|
||||
|
||||
## [1.120.0] - 2023-08-28
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add `resend_messages`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update async-imap to 0.9.1 to fix memory leak.
|
||||
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
|
||||
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
|
||||
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Hide accounts.rs constants from public API.
|
||||
- Hide pgp module from public API.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update to Zig 0.11.0.
|
||||
- Update to Rust 1.72.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Run on push to stable branch.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- python: Fix lint errors.
|
||||
- python: Fix `ruff` 0.0.286 warnings.
|
||||
- Fix beta clippy warnings.
|
||||
|
||||
## [1.119.1] - 2023-08-06
|
||||
|
||||
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
|
||||
|
||||
### Tests
|
||||
|
||||
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
|
||||
|
||||
## [1.119.0] - 2023-08-03
|
||||
|
||||
### Fixes
|
||||
|
||||
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
|
||||
- imap: Do not resync IMAP after initial configuration.
|
||||
|
||||
- webxdc: Accept WebXDC updates in mailing lists.
|
||||
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
|
||||
- webxdc: Delete old webxdc status updates during housekeeping.
|
||||
|
||||
- Return valid MsgId from `receive_imf()` when the message is replaced.
|
||||
- Emit MsgsChanged event with correct chat id for replaced messages.
|
||||
|
||||
- deltachat-rpc-server: Update tokio-tar to fix backup import.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-rpc-client: Add `MSG_DELETED` constant.
|
||||
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
|
||||
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update default value for `show_emails` in `dc_set_config()` documentation.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Improve IMAP logs.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add basic import/export test for async python.
|
||||
- Add `test_webxdc_download_on_demand`.
|
||||
- Add tests for deletion of webxdc status-updates.
|
||||
|
||||
## [1.118.0] - 2023-07-07
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
|
||||
- Add `Contact::get_by_id_optional()` API.
|
||||
- [**breaking**] Make `Message.text` non-optional.
|
||||
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
|
||||
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
|
||||
Previously `handle_mdn` was erroneously exposed in the public API.
|
||||
- python: flatten the API of `deltachat` module.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use different member added/removal messages locally and on the network.
|
||||
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
|
||||
- Make SVG avatar image work on more platforms (use `xlink:href`).
|
||||
- Preserve indentation when converting plaintext to HTML.
|
||||
- Do not run simplify() on dehtml() output.
|
||||
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document how to regenerate Node.js constants before the release.
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: Do not fail if commit.footers is undefined.
|
||||
|
||||
### Other
|
||||
|
||||
- Dependency updates.
|
||||
- Update MPL 2.0 license text.
|
||||
- Add LICENSE file to deltachat-rpc-client.
|
||||
- deltachat-rpc-client: Add Trove classifiers.
|
||||
- python: Change bindings status to production/stable.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add `make-python-testenv.sh` script.
|
||||
|
||||
## [1.117.0] - 2023-06-15
|
||||
|
||||
### Features
|
||||
|
||||
- New group membership update algorithm.
|
||||
|
||||
New algorithm improves group consistency
|
||||
in cases of missing messages,
|
||||
restored old backups and replies from classic MUAs.
|
||||
|
||||
- Add `DC_EVENT_MSG_DELETED` event.
|
||||
|
||||
This event notifies the UI about the message
|
||||
being deleted from the messagelist, e.g. when the message expires
|
||||
or the user deletes it.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
|
||||
|
||||
Specifying msg IDs that cannot be loaded in the event payload
|
||||
results in an error when the UI tries to load the message.
|
||||
Instead, emit an event without IDs
|
||||
to make the UI reload the whole messagelist.
|
||||
|
||||
- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
|
||||
|
||||
This bug resulted in failure to propagate verification
|
||||
if the contact list already contained a new verified group member
|
||||
with a non-lowercase address.
|
||||
|
||||
- dehtml: skip links with empty text.
|
||||
|
||||
Links like `<a href="https://delta.chat/"></a>` in HTML mails are now skipped
|
||||
instead of being converted to a link without a label like `[](https://delta.chat/)`.
|
||||
|
||||
- dehtml: Do not insert unnecessary newlines when parsing `<p>` tags.
|
||||
|
||||
- Update from yanked `libc` 0.2.145 to 0.2.146.
|
||||
- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
|
||||
|
||||
Previously a single event with zero chat ID was emitted.
|
||||
|
||||
- python: make `Contact.is_verified()` return bool.
|
||||
|
||||
- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
|
||||
|
||||
- rust: make `WebxdcManifest` type public.
|
||||
|
||||
### Build system
|
||||
|
||||
- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
|
||||
- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
|
||||
|
||||
### CI
|
||||
|
||||
- Run `cargo check` with musl libc.
|
||||
- concourse: Install devpi in a virtual environment.
|
||||
- Remove [mergeable](https://mergeable.us/) configuration.
|
||||
|
||||
### Documentation
|
||||
|
||||
- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
|
||||
- CONTRIBUTING: document how conventional commits interact with squash merges.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename `MimeMessage.header` into `MimeMessage.headers`.
|
||||
|
||||
- Derive `Default` trait for `WebxdcManifest`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Regression test for case-sensitive comparison of gossip header to contact address.
|
||||
- Multiple new group consistency tests in Rust.
|
||||
- python: Replace legacy `tmpdir` fixture with `tmp_path`.
|
||||
|
||||
## [1.116.0] - 2023-06-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `dc_jsonrpc_blocking_call()`.
|
||||
|
||||
### Changes
|
||||
|
||||
- Generate OpenRPC definitions for JSON-RPC.
|
||||
- Add more context to message loading errors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Build deltachat-node prebuilds on Debian 10.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document release process in `RELEASE.md`.
|
||||
- Add contributing guidelines `CONTRIBUTING.md`.
|
||||
- Update instructions for python devenv.
|
||||
- python: Document pytest fixtures.
|
||||
|
||||
### Tests
|
||||
|
||||
- python: Make `test_mdn_asymmetric` less flaky.
|
||||
- Make `test_group_with_removed_message_id` less flaky.
|
||||
- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: Changelog generation improvements.
|
||||
- `set_core_version.py`: Expect release date in the changelog.
|
||||
|
||||
### CI
|
||||
|
||||
- Require Python 3.8 for deltachat-rpc-client.
|
||||
- mergeable: Allow PR titles to start with "ci" and "build".
|
||||
- Remove incorrect comment.
|
||||
- dependabot: Use `chore` prefix for dependency updates.
|
||||
- Remove broken `node-delete-preview.yml` workflow.
|
||||
- Add top comments to GH Actions workflows.
|
||||
- Run node.js lint on Windows.
|
||||
- Update clippy to 1.70.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove release.toml.
|
||||
- gitattributes: Configure LF line endings for JavaScript files.
|
||||
- Update dependencies
|
||||
|
||||
## [1.112.10] - 2023-06-01
|
||||
|
||||
### Fixes
|
||||
|
||||
- Disable `fetch_existing_msgs` setting by default.
|
||||
- Update `h2` to fix RUSTSEC-2023-0034.
|
||||
|
||||
## [1.115.0] - 2023-05-12
|
||||
|
||||
### JSON-RPC API Changes
|
||||
@@ -2486,6 +2754,13 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
|
||||
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
|
||||
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
|
||||
[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
|
||||
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
|
||||
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
|
||||
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
|
||||
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
|
||||
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
|
||||
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
|
||||
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
|
||||
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
|
||||
92
CONTRIBUTING.md
Normal file
92
CONTRIBUTING.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Contributing guidelines
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||
If the bug you found is specific to
|
||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||
report it to the corresponding repository.
|
||||
|
||||
## Proposing features
|
||||
|
||||
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||
|
||||
## Contributing code
|
||||
|
||||
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
|
||||
If you have write access to the repository,
|
||||
push a branch named `<username>/<feature>`
|
||||
so it is clear who is responsible for the branch,
|
||||
and open a PR proposing to merge the change.
|
||||
Otherwise fork the repository and create a branch in your fork.
|
||||
|
||||
You can find the list of good first issues
|
||||
and a link to this guide
|
||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||
|
||||
### Coding conventions
|
||||
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||
|
||||
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
- `test`: Test changes and improvements to the testing framework.
|
||||
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||
|
||||
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
|
||||
|
||||
If you intend to squash merge the PR from the web interface,
|
||||
make sure the PR title follows the conventional commits notation
|
||||
as it will end up being a commit title.
|
||||
Otherwise make sure each commit title follows the conventional commit notation.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||
|
||||
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||
|
||||
```
|
||||
fix: Fix race condition and db corruption when a message was received during backup
|
||||
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
#### Multiple Changes in one PR
|
||||
|
||||
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
|
||||
|
||||
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
### Reviewing
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
|
||||
This is to ensure that PRs are merged as intended by the author,
|
||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||
|
||||
If you do not have access to the repository and created a PR from a fork,
|
||||
ask the maintainers to merge the PR and say how it should be merged.
|
||||
|
||||
## Other ways to contribute
|
||||
|
||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||
1577
Cargo.lock
generated
1577
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.115.0"
|
||||
version = "1.120.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
@@ -23,6 +23,7 @@ opt-level = "z"
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
|
||||
[patch.crates-io]
|
||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
@@ -35,13 +36,13 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
async-channel = "1.8.0"
|
||||
async-imap = { version = "0.8.0", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
brotli = "3.3"
|
||||
brotli = { version = "3.3", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
@@ -58,19 +59,19 @@ lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master"
|
||||
libc = "0.2"
|
||||
mailparse = "0.14"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.15"
|
||||
num-derive = "0.3"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.17.0"
|
||||
percent-encoding = "2.2"
|
||||
once_cell = "1.18.0"
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.28"
|
||||
quick-xml = "0.29"
|
||||
rand = "0.8"
|
||||
regex = "1.8"
|
||||
reqwest = { version = "0.11.17", features = ["json"] }
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.4"
|
||||
@@ -79,8 +80,8 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.0"
|
||||
thiserror = "1"
|
||||
@@ -96,14 +97,15 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "1.13"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.7.3"
|
||||
testdir = "0.8.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
|
||||
@@ -167,8 +167,8 @@ Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js**
|
||||
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||
- **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/)\]
|
||||
|
||||
21
RELEASE.md
Normal file
21
RELEASE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Releasing a new version of DeltaChat core
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||
|
||||
2. Run `npm run build:core:constants` in the root of the repository
|
||||
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
||||
|
||||
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
@@ -67,9 +67,11 @@ body = """
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||
{{ commit.message | upper_first }}.\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
{% endif %}{% endfor %}\
|
||||
{% if commit.footers is defined %}\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
{% endif %}{% endfor %}\
|
||||
{% endif%}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.115.0"
|
||||
version = "1.120.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -24,8 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
once_cell = "1.17.0"
|
||||
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
|
||||
once_cell = "1.18.0"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -420,11 +420,11 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 0=watch all folders normally (default)
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only (default),
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts.
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||
* generate recommended key type (default),
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
@@ -3978,16 +3978,17 @@ char* dc_msg_get_text (const dc_msg_t* msg);
|
||||
*/
|
||||
char* dc_msg_get_subject (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Find out full path, file name and extension of the file associated with a
|
||||
* message.
|
||||
* Find out full path of the file associated with a message.
|
||||
*
|
||||
* Typically files are associated with images, videos, audios, documents.
|
||||
* Plain text messages do not have a file.
|
||||
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The full path, the file name, and the extension of the file associated with the message.
|
||||
* @return The full path (with file name and extension) of the file associated with the message.
|
||||
* If there is no file associated with the message, an empty string is returned.
|
||||
* NULL is never returned and the returned value must be released using dc_str_unref().
|
||||
*/
|
||||
@@ -3995,14 +3996,13 @@ char* dc_msg_get_file (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a base file name without the path. The base file name includes the extension; the path
|
||||
* is not returned. To get the full path, use dc_msg_get_file().
|
||||
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||
* use dc_msg_get_file().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The base file name plus the extension without part. If there is no file
|
||||
* associated with the message, an empty string is returned. The returned
|
||||
* value must be released using dc_str_unref().
|
||||
* @return The attachment filename. If there is no file associated with the message, an empty string
|
||||
* is returned. The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_msg_get_filename (const dc_msg_t* msg);
|
||||
|
||||
@@ -6079,6 +6079,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_MSG_READ 2015
|
||||
|
||||
|
||||
/**
|
||||
* A single message is deleted.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELETED 2016
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -527,6 +527,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
EventType::MsgRead { .. } => 2015,
|
||||
EventType::MsgDeleted { .. } => 2016,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
@@ -574,6 +575,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgDelivered { chat_id, .. }
|
||||
| EventType::MsgFailed { chat_id, .. }
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::MsgDeleted { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
@@ -631,7 +633,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| EventType::MsgRead { msg_id, .. }
|
||||
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
@@ -674,6 +677,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::MsgDelivered { .. }
|
||||
| EventType::MsgFailed { .. }
|
||||
| EventType::MsgRead { .. }
|
||||
| EventType::MsgDeleted { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
@@ -1873,13 +1877,10 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
message::get_msg_info(ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||
.strdup()
|
||||
})
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
block_on(msg_id.get_info(ctx))
|
||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3304,7 +3305,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_text().unwrap_or_default().strdup()
|
||||
ffi_msg.message.get_text().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3689,7 +3690,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.set_text(to_opt_string_lossy(text))
|
||||
ffi_msg.message.set_text(to_string_lossy(text))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
1
deltachat-jsonrpc/.gitignore
vendored
1
deltachat-jsonrpc/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
openrpc/openrpc.json
|
||||
accounts/
|
||||
|
||||
.cargo
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.115.0"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -15,15 +15,16 @@ required-features = ["webserver"]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
tempfile = "3.6.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.28" }
|
||||
serde_json = "1.0.96"
|
||||
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
|
||||
serde_json = "1.0.99"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.28.0" }
|
||||
tokio = { version = "1.29.1" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
@@ -33,7 +34,7 @@ axum = { version = "0.6.18", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -19,9 +19,7 @@ use deltachat::{
|
||||
context::get_info,
|
||||
ephemeral::Timer,
|
||||
imex, location,
|
||||
message::{
|
||||
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
},
|
||||
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
|
||||
provider::get_provider_info,
|
||||
qr,
|
||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
||||
@@ -144,7 +142,11 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
#[rpc(
|
||||
all_positional,
|
||||
ts_outdir = "typescript/generated",
|
||||
openrpc_outdir = "openrpc"
|
||||
)]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -897,7 +899,7 @@ impl CommandApi {
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(text));
|
||||
msg.set_text(text);
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||
Ok(message_id.to_u32())
|
||||
@@ -1115,7 +1117,7 @@ impl CommandApi {
|
||||
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_msg_info(&ctx, MsgId::new(message_id)).await
|
||||
MsgId::new(message_id).get_info(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns contacts that sent read receipts and the time of reading.
|
||||
@@ -1336,7 +1338,7 @@ impl CommandApi {
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
let contact = Contact::load_from_db(&ctx, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
Contact::create(&ctx, &name, addr).await?;
|
||||
Ok(())
|
||||
@@ -1707,6 +1709,20 @@ impl CommandApi {
|
||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Resend messages and make information available for newly added chat members.
|
||||
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
/// Clients that already have the original message can still ignore the resent message as
|
||||
/// they have tracked the state by dedicated updates.
|
||||
///
|
||||
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||
///
|
||||
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
|
||||
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
chat::resend_msgs(&ctx, &message_ids).await
|
||||
}
|
||||
|
||||
async fn send_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1763,9 +1779,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if data.text.is_some() {
|
||||
message.set_text(data.text);
|
||||
}
|
||||
message.set_text(data.text.unwrap_or_default());
|
||||
if data.html.is_some() {
|
||||
message.set_html(data.html);
|
||||
}
|
||||
@@ -1946,7 +1960,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(text));
|
||||
msg.set_text(text);
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
@@ -1969,9 +1983,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if text.is_some() {
|
||||
message.set_text(text);
|
||||
}
|
||||
message.set_text(text.unwrap_or_default());
|
||||
if let Some(file) = file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
@@ -2015,9 +2027,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if text.is_some() {
|
||||
draft.set_text(text);
|
||||
}
|
||||
draft.set_text(text.unwrap_or_default());
|
||||
if let Some(file) = file {
|
||||
draft.set_file(file, None);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Account {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -13,7 +13,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullChat {
|
||||
id: u32,
|
||||
@@ -53,7 +53,7 @@ impl FullChat {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
context,
|
||||
Contact::load_from_db(context, *contact_id)
|
||||
Contact::get_by_id(context, *contact_id)
|
||||
.await
|
||||
.context("failed to load contact")?,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ impl FullChat {
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.get(0) {
|
||||
Some(contact) => Contact::load_from_db(context, *contact)
|
||||
Some(contact) => Contact::get_by_id(context, *contact)
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
@@ -121,7 +121,7 @@ impl FullChat {
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasicChat {
|
||||
id: u32,
|
||||
@@ -166,7 +166,7 @@ impl BasicChat {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -191,7 +191,7 @@ impl MuteDuration {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatVisibility")]
|
||||
pub enum JSONRPCChatVisibility {
|
||||
Normal,
|
||||
|
||||
@@ -8,13 +8,16 @@ use deltachat::{
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub struct ChatListEntry(pub u32, pub u32);
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -104,7 +107,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let contact = chat_contacts.get(0);
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
|
||||
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
pub struct ContactObject {
|
||||
address: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Event {
|
||||
/// Event payload.
|
||||
@@ -21,7 +21,7 @@ impl From<CoreEvent> for Event {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
@@ -174,6 +174,13 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDeleted {
|
||||
chat_id: u32,
|
||||
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.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
@@ -347,6 +354,10 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::ChatModified(chat_id) => ChatModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::net::HttpResponse as CoreHttpResponse;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub struct HttpResponse {
|
||||
/// base64-encoded response body.
|
||||
blob: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::location::Location;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Location", rename_all = "camelCase")]
|
||||
pub struct JsonrpcLocation {
|
||||
pub location_id: u32,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat::chat::Chat;
|
||||
use deltachat::chat::ChatItem;
|
||||
use deltachat::chat::ChatVisibility;
|
||||
@@ -10,8 +10,7 @@ use deltachat::message::MsgId;
|
||||
use deltachat::message::Viewtype;
|
||||
use deltachat::reaction::get_msg_reactions;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
@@ -19,14 +18,14 @@ use super::contact::ContactObject;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
||||
pub enum MessageLoadResult {
|
||||
Message(MessageObject),
|
||||
LoadingError { error: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||
pub struct MessageObject {
|
||||
id: u32,
|
||||
@@ -35,7 +34,7 @@ pub struct MessageObject {
|
||||
quote: Option<MessageQuote>,
|
||||
parent_id: Option<u32>,
|
||||
|
||||
text: Option<String>,
|
||||
text: String,
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
view_type: MessageViewtype,
|
||||
@@ -86,7 +85,7 @@ pub struct MessageObject {
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
enum MessageQuote {
|
||||
JustText {
|
||||
@@ -114,8 +113,12 @@ impl MessageObject {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||
.await
|
||||
.context("failed to load sender contact")?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
|
||||
.await
|
||||
.context("failed to load sender contact object")?;
|
||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
@@ -132,7 +135,9 @@ impl MessageObject {
|
||||
let quote = if let Some(quoted_text) = message.quoted_text() {
|
||||
match message.quoted_message(context).await? {
|
||||
Some(quote) => {
|
||||
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
|
||||
let quote_author = Contact::get_by_id(context, quote.get_from_id())
|
||||
.await
|
||||
.context("failed to load quote author contact")?;
|
||||
Some(MessageQuote::WithMessage {
|
||||
text: quoted_text,
|
||||
message_id: quote.get_id().to_u32(),
|
||||
@@ -160,7 +165,9 @@ impl MessageObject {
|
||||
None
|
||||
};
|
||||
|
||||
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||
let reactions = get_msg_reactions(context, msg_id)
|
||||
.await
|
||||
.context("failed to load message reactions")?;
|
||||
let reactions = if reactions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -180,7 +187,7 @@ impl MessageObject {
|
||||
state: message
|
||||
.get_state()
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
||||
.context("state conversion to number failed")?,
|
||||
error: message.error(),
|
||||
|
||||
timestamp: message.get_timestamp(),
|
||||
@@ -203,7 +210,7 @@ impl MessageObject {
|
||||
videochat_type: match message.get_videochat_type() {
|
||||
Some(vct) => Some(
|
||||
vct.to_u32()
|
||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
||||
.context("videochat type conversion to number failed")?,
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
@@ -230,7 +237,7 @@ impl MessageObject {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Viewtype")]
|
||||
pub enum MessageViewtype {
|
||||
Unknown,
|
||||
@@ -306,7 +313,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum DownloadState {
|
||||
Done,
|
||||
Available,
|
||||
@@ -325,7 +332,7 @@ impl From<download::DownloadState> for DownloadState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
@@ -380,7 +387,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageNotificationInfo {
|
||||
id: u32,
|
||||
@@ -438,7 +445,7 @@ impl MessageNotificationInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageSearchResult {
|
||||
id: u32,
|
||||
@@ -462,7 +469,7 @@ impl MessageSearchResult {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
|
||||
|
||||
let profile_image = match sender.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
@@ -493,13 +500,13 @@ impl MessageSearchResult {
|
||||
is_chat_protected: chat.is_protected(),
|
||||
is_chat_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
message: message.get_text().unwrap_or_default(),
|
||||
message: message.get_text(),
|
||||
timestamp: message.get_timestamp(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||
pub enum JSONRPCMessageListItem {
|
||||
Message {
|
||||
@@ -525,7 +532,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TypeDef)]
|
||||
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageData {
|
||||
pub text: Option<String>,
|
||||
@@ -537,7 +544,7 @@ pub struct MessageData {
|
||||
pub quoted_message_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageReadReceipt {
|
||||
pub contact_id: u32,
|
||||
|
||||
@@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderInfo {
|
||||
pub before_login_hint: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
/// A single reaction emoji.
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReaction {
|
||||
/// Emoji.
|
||||
@@ -20,7 +20,7 @@ pub struct JSONRPCReaction {
|
||||
}
|
||||
|
||||
/// Structure representing all reactions to a particular message.
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
|
||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::maybe_empty_string_to_option;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
||||
pub struct WebxdcMessageInfo {
|
||||
/// The name of the app.
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.115.0"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.115.0"
|
||||
version = "1.120.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.16"
|
||||
pretty_env_logger = "0.4"
|
||||
log = "0.4.19"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.29"
|
||||
rustyline = "11"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -199,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
msgtext,
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == ContactId::SELF {
|
||||
""
|
||||
@@ -912,9 +912,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Viewtype::File
|
||||
});
|
||||
msg.set_file(arg1, None);
|
||||
if !arg2.is_empty() {
|
||||
msg.set_text(Some(arg2.to_string()));
|
||||
}
|
||||
msg.set_text(arg2.to_string());
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendhtml" => {
|
||||
@@ -926,11 +924,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_html(Some(html.to_string()));
|
||||
msg.set_text(Some(if arg2.is_empty() {
|
||||
msg.set_text(if arg2.is_empty() {
|
||||
path.file_name().unwrap().to_string_lossy().to_string()
|
||||
} else {
|
||||
arg2.to_string()
|
||||
}));
|
||||
});
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||
@@ -979,7 +977,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
if !arg1.is_empty() {
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(Some(arg1.to_string()));
|
||||
draft.set_text(arg1.to_string());
|
||||
sel_chat
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -1003,7 +1001,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"Please specify text to add as device message."
|
||||
);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(arg1.to_string()));
|
||||
msg.set_text(arg1.to_string());
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
"listmedia" => {
|
||||
@@ -1090,7 +1088,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
let res = id.get_info(&context).await?;
|
||||
println!("{res}");
|
||||
}
|
||||
"download" => {
|
||||
|
||||
373
deltachat-rpc-client/LICENSE
Normal file
373
deltachat-rpc-client/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -9,6 +9,21 @@ dependencies = [
|
||||
"aiohttp",
|
||||
"aiodns"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: AsyncIO",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
@@ -259,3 +259,11 @@ class Account:
|
||||
)
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
await self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
|
||||
async def import_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Import backup."""
|
||||
await self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
|
||||
@@ -45,6 +45,7 @@ class EventType(str, Enum):
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||
CONTACTS_CHANGED = "ContactsChanged"
|
||||
|
||||
@@ -16,9 +16,8 @@ async def get_temp_credentials() -> dict:
|
||||
|
||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
|
||||
|
||||
class ACFactory:
|
||||
|
||||
@@ -101,6 +101,16 @@ async def test_account(acfactory) -> None:
|
||||
assert await alice.get_fresh_messages()
|
||||
assert await alice.get_next_messages()
|
||||
|
||||
# Test sending empty message.
|
||||
assert len(await bob.wait_next_messages()) == 0
|
||||
await alice_chat_bob.send_text("")
|
||||
messages = await bob.wait_next_messages()
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = await message.get_snapshot()
|
||||
assert snapshot.text == ""
|
||||
await bob.mark_seen_messages([message])
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
group_msg = await group.send_message(text="hello")
|
||||
@@ -335,3 +345,13 @@ async def test_wait_next_messages(acfactory) -> None:
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
await alice.export_backup(tmp_path)
|
||||
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = await acfactory.get_unconfigured_account()
|
||||
await alice2.import_backup(files[0])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.115.0"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -17,11 +17,11 @@ anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "1.13.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.96"
|
||||
serde_json = "1.0.99"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.28.0", features = ["io-std"] }
|
||||
tokio = { version = "1.29.1", features = ["io-std"] }
|
||||
tokio-util = "0.7.8"
|
||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Delta Chat core RPC server.
|
||||
//!
|
||||
//! It speaks JSON Lines over stdio.
|
||||
use std::env;
|
||||
///! Delta Chat core RPC server.
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
22
deny.toml
22
deny.toml
@@ -2,9 +2,12 @@
|
||||
unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
|
||||
# Only affects windows if using non-default allocator (and unmaintained).
|
||||
"RUSTSEC-2021-0145",
|
||||
# Exponential CPU time usage for TLS certificate processing in webpki.
|
||||
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
|
||||
# Waiting for `iroh` to update dependencies.
|
||||
"RUSTSEC-2023-0052",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -13,12 +16,11 @@ ignore = [
|
||||
# when upgrading.
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "ahash", version = "0.7.6" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "clap_lex", version = "0.2.4" },
|
||||
{ name = "clap", version = "3.2.23" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
@@ -28,12 +30,12 @@ skip = [
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "<0.10" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hermit-abi", version = "<0.3" },
|
||||
{ name = "humantime", version = "<2.1" },
|
||||
{ name = "hashbrown", version = "<0.14.0" },
|
||||
{ name = "idna", version = "<0.3" },
|
||||
{ name = "libm", version = "0.1.4" },
|
||||
{ name = "indexmap", version = "<2.0.0" },
|
||||
{ name = "linux-raw-sys", version = "0.3.8" },
|
||||
{ name = "num-derive", version = "0.3.3" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
@@ -42,9 +44,11 @@ skip = [
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustix", version = "0.37.21" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "socket2", version = "0.4.9" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
@@ -57,8 +61,10 @@ skip = [
|
||||
{ name = "windows-sys", version = "<0.48" },
|
||||
{ name = "windows-targets", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||
{ name = "winreg", version = "0.10.1" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
128
fuzz/Cargo.lock
generated
128
fuzz/Cargo.lock
generated
@@ -2,12 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "abao"
|
||||
version = "0.2.0"
|
||||
@@ -60,19 +54,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@@ -189,12 +177,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25"
|
||||
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-native-tls 0.4.0",
|
||||
"base64 0.21.0",
|
||||
"byte-pool",
|
||||
"chrono",
|
||||
@@ -203,25 +190,13 @@ dependencies = [
|
||||
"log",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"ouroboros",
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
"stop-token",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-native-tls"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-native-tls"
|
||||
version = "0.5.0"
|
||||
@@ -556,9 +531,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "byte-pool"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
|
||||
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"stable_deref_trait",
|
||||
@@ -951,12 +926,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.112.6"
|
||||
version = "1.117.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"async-imap",
|
||||
"async-native-tls 0.5.0",
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"backtrace",
|
||||
@@ -979,6 +954,7 @@ dependencies = [
|
||||
"lettre_email",
|
||||
"libc",
|
||||
"mailparse 0.14.0",
|
||||
"mime",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
@@ -1244,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature 1.6.4",
|
||||
"signature 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1701,9 +1677,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
|
||||
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
@@ -2397,9 +2373,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
@@ -2651,9 +2627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -2663,9 +2639,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.48"
|
||||
version = "0.10.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
|
||||
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
@@ -2704,11 +2680,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.83"
|
||||
version = "0.9.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
|
||||
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
@@ -2716,29 +2691,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -2865,9 +2817,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||
|
||||
[[package]]
|
||||
name = "pgp"
|
||||
@@ -3316,13 +3268,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.0"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3331,7 +3283,7 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3341,10 +3293,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.16"
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
@@ -3690,6 +3648,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
@@ -4249,9 +4213,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.11"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -4275,9 +4239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
||||
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
|
||||
@@ -48,6 +48,7 @@ module.exports = {
|
||||
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,
|
||||
|
||||
@@ -22,6 +22,7 @@ module.exports = {
|
||||
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',
|
||||
|
||||
@@ -48,6 +48,7 @@ export enum C {
|
||||
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,
|
||||
@@ -307,6 +308,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
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',
|
||||
|
||||
@@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.endsWith(image)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.115.0"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
high level API reference
|
||||
========================
|
||||
|
||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||
- :class:`deltachat.Account` (your main entry point, creates the
|
||||
other classes)
|
||||
- :class:`deltachat.contact.Contact`
|
||||
- :class:`deltachat.chat.Chat`
|
||||
- :class:`deltachat.message.Message`
|
||||
- :class:`deltachat.Contact`
|
||||
- :class:`deltachat.Chat`
|
||||
- :class:`deltachat.Message`
|
||||
|
||||
Account
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.account.Account
|
||||
.. autoclass:: deltachat.Account
|
||||
:members:
|
||||
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.contact.Contact
|
||||
.. autoclass:: deltachat.Contact
|
||||
:members:
|
||||
|
||||
Chat
|
||||
----
|
||||
|
||||
.. autoclass:: deltachat.chat.Chat
|
||||
.. autoclass:: deltachat.Chat
|
||||
:members:
|
||||
|
||||
Message
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.message.Message
|
||||
.. autoclass:: deltachat.Message
|
||||
:members:
|
||||
|
||||
|
||||
@@ -32,25 +32,13 @@ class GroupTrackingPlugin:
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
bot_chat = bot_contact.create_chat()
|
||||
@@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
|
||||
@@ -11,10 +11,11 @@ authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ from array import array
|
||||
from contextlib import contextmanager
|
||||
from email.utils import parseaddr
|
||||
from threading import Event
|
||||
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
|
||||
|
||||
from . import const, hookspec
|
||||
from .capi import ffi, lib
|
||||
@@ -427,7 +427,7 @@ class Account:
|
||||
|
||||
assert dc_chatlist != ffi.NULL
|
||||
chatlist = []
|
||||
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
@@ -71,9 +71,9 @@ class Contact:
|
||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self):
|
||||
def is_verified(self) -> bool:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact):
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
|
||||
@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
for i in range(lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ from contextlib import contextmanager
|
||||
from queue import Empty, Queue
|
||||
|
||||
from . import const
|
||||
from .account import Account
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
from .hookspec import account_hookimpl
|
||||
from .message import map_system_message
|
||||
from .account import Account
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
|
||||
@@ -486,6 +486,9 @@ class Message:
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
def download_full(self) -> None:
|
||||
lib.dc_download_full_msg(self.account._dc_context, self.id)
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
@@ -507,8 +510,7 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
|
||||
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import threading
|
||||
import time
|
||||
import weakref
|
||||
from queue import Queue
|
||||
from typing import Callable, List, Optional, Dict, Set
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -137,6 +137,9 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def testprocess(request):
|
||||
"""Return live account configuration manager.
|
||||
|
||||
The returned object is a :class:`TestProcess` object."""
|
||||
return TestProcess(pytestconfig=request.config)
|
||||
|
||||
|
||||
@@ -231,6 +234,8 @@ def write_dict_to_dir(dic, target_dir):
|
||||
|
||||
@pytest.fixture()
|
||||
def data(request):
|
||||
"""Test data."""
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
@@ -614,6 +619,7 @@ class ACFactory:
|
||||
|
||||
@pytest.fixture()
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
"""Account factory."""
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
@@ -676,21 +682,17 @@ class BotProcess:
|
||||
print("+++IGN:", line)
|
||||
ignored.append(line)
|
||||
|
||||
def await_resync(self):
|
||||
self.fnmatch_lines(
|
||||
"""
|
||||
*Resync: collected * message IDs in folder INBOX*
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_db_path(tmpdir):
|
||||
"""Return a path inside the temporary directory where the database can be created."""
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lp():
|
||||
"""Log printer fixture."""
|
||||
|
||||
class Printer:
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from .hookspec import Global, account_hookimpl
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(BENCH_NUM)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
|
||||
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
import deltachat
|
||||
|
||||
|
||||
def test_db_busy_error(acfactory, tmpdir):
|
||||
def test_db_busy_error(acfactory):
|
||||
starttime = time.time()
|
||||
log_lock = threading.RLock()
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ def test_multidevice_sync_seen(acfactory, lp):
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
|
||||
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
@@ -507,9 +507,10 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
for ac in [ac1, ac1_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
ac1.export_self_keys(dir.strpath)
|
||||
ac1_offl.import_self_keys(dir.strpath)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac1.export_self_keys(str(dir))
|
||||
ac1_offl.import_self_keys(str(dir))
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
@@ -541,7 +542,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
@@ -556,9 +557,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
ac2.export_self_keys(dir.strpath)
|
||||
ac2_offl.import_self_keys(dir.strpath)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
|
||||
@@ -2,18 +2,18 @@ import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
from deltachat import const
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmpdir):
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
imap2.mark_all_read()
|
||||
assert imap2.get_unread_cnt() == 0
|
||||
|
||||
imap2.dump_imap_structures(tmpdir, logfile=sys.stdout)
|
||||
imap2.dump_imap_structures(tmp_path, logfile=sys.stdout)
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
|
||||
acfactory.bring_accounts_online()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
@@ -72,35 +72,37 @@ def test_configure_canceled(acfactory):
|
||||
pass
|
||||
|
||||
|
||||
def test_configure_unref(tmpdir):
|
||||
def test_configure_unref(tmp_path):
|
||||
"""Test that removing the last reference to the context during ongoing configuration
|
||||
does not result in use-after-free."""
|
||||
from deltachat.capi import ffi, lib
|
||||
|
||||
path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath
|
||||
dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL)
|
||||
path = tmp_path / "test_configure_unref"
|
||||
path.mkdir()
|
||||
dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").encode("utf8"), ffi.NULL)
|
||||
lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8"))
|
||||
lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8"))
|
||||
lib.dc_configure(dc_context)
|
||||
lib.dc_context_unref(dc_context)
|
||||
|
||||
|
||||
def test_export_import_self_keys(acfactory, tmpdir, lp):
|
||||
def test_export_import_self_keys(acfactory, tmp_path, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
export_files = ac1.export_self_keys(dir.strpath)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
export_files = ac1.export_self_keys(str(dir))
|
||||
assert len(export_files) == 2
|
||||
for x in export_files:
|
||||
assert x.startswith(dir.strpath)
|
||||
assert x.startswith(str(dir))
|
||||
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
ac1._evtracker.consume_events()
|
||||
|
||||
lp.sec("exported keys (private and public)")
|
||||
for name in os.listdir(dir.strpath):
|
||||
lp.indent(dir.strpath + os.sep + name)
|
||||
for name in dir.iterdir():
|
||||
lp.indent(str(dir / name))
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(dir.strpath)
|
||||
ac2.import_self_keys(str(dir))
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
|
||||
assert key_id2 == key_id
|
||||
|
||||
@@ -156,62 +158,65 @@ def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
|
||||
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "somedäüta.html.zip"
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
basename = "somedäüta"
|
||||
ext = ".html.zip"
|
||||
p = tmp_path / (basename + ext)
|
||||
p.write_text("some data")
|
||||
|
||||
def send_and_receive_message():
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
msg1 = Message.new_empty(ac1, "file")
|
||||
msg1.set_text("withfile")
|
||||
msg1.set_file(p)
|
||||
msg1.set_file(str(p))
|
||||
chat.send_msg(msg1)
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return ac2.get_message_by_id(ev.data2)
|
||||
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
assert msg2.filename.endswith("html.zip")
|
||||
msg2.filename.index(basename)
|
||||
assert msg2.filename.endswith(ext)
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
|
||||
def test_send_file_html_attachment(tmpdir, acfactory, lp):
|
||||
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "test.html"
|
||||
basename = "test"
|
||||
ext = ".html"
|
||||
content = "<html><body>text</body>data"
|
||||
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
f.write(content)
|
||||
p = tmp_path / (basename + ext)
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
p.write_text(content)
|
||||
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
chat.send_file(p, mime_type="text/html")
|
||||
chat.send_file(str(p), mime_type="text/html")
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
|
||||
def test_html_message(acfactory, lp):
|
||||
@@ -324,6 +329,59 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_webxdc_huge_update(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
payload = "A" * 1000
|
||||
assert msg1.send_status_update({"payload": payload}, "some test data")
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
update = msg2.get_status_updates()[0]
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
|
||||
ac2_update.download_full()
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
assert msg2.get_status_updates()
|
||||
|
||||
# Get a event notifying that the message disappeared from the chat.
|
||||
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert msgs_changed_event.data1 == msg2.chat.id
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
@@ -351,7 +409,42 @@ def test_move_works(acfactory):
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory):
|
||||
"""Test that the message is only moved once.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
|
||||
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
# Message is moved to the DeltaChat folder and downloaded.
|
||||
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX again.
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Check that Message 1 is still in the INBOX folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2.direct_imap.select_folder("INBOX")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
@@ -396,9 +489,12 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 1
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED")
|
||||
assert ev.data2 == messages[0].id
|
||||
assert not chat3.get_messages()
|
||||
|
||||
|
||||
@@ -531,8 +627,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
lp.step("1")
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
|
||||
lp.step("2")
|
||||
|
||||
# Check that ac1 marks the read receipt as read.
|
||||
@@ -703,7 +799,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.")
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
@@ -1204,7 +1300,7 @@ def test_quote_encrypted(acfactory, lp):
|
||||
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
|
||||
|
||||
|
||||
def test_quote_attachment(tmpdir, acfactory, lp):
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
"""Test that replies with an attachment and a quote are received correctly."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1219,15 +1315,14 @@ def test_quote_attachment(tmpdir, acfactory, lp):
|
||||
assert received_message.text == "hi"
|
||||
|
||||
basename = "attachment.txt"
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
f.write("data to send")
|
||||
p = tmp_path / basename
|
||||
p.write_text("data to send")
|
||||
|
||||
lp.sec("ac2 sends a reply to ac1")
|
||||
chat2 = received_message.create_chat()
|
||||
reply = Message.new_empty(ac2, "file")
|
||||
reply.set_text("message reply")
|
||||
reply.set_file(p)
|
||||
reply.set_file(str(p))
|
||||
reply.quote = received_message
|
||||
chat2.send_msg(reply)
|
||||
|
||||
@@ -1334,7 +1429,7 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
@@ -1369,10 +1464,9 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmpdir.join("large")
|
||||
with open(path, "wb") as fout:
|
||||
fout.write(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(path.strpath))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
@@ -1385,7 +1479,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
@@ -1431,7 +1525,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
@@ -1443,10 +1537,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
chat1.send_image(original_image_path)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmpdir.join("attachment.txt")
|
||||
with open(path, "w") as file:
|
||||
path = tmp_path / "attachment.txt"
|
||||
with path.open("w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(path.strpath)
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
@@ -1464,12 +1558,13 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
lp.sec(f"export all to {backupdir}")
|
||||
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac1.stop_io()
|
||||
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
|
||||
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
|
||||
|
||||
# check progress events for export
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
@@ -1487,7 +1582,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
lp.sec("get latest backup file")
|
||||
path2 = ac2.get_latest_backupfile(backupdir.strpath)
|
||||
path2 = ac2.get_latest_backupfile(str(backupdir))
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
@@ -1505,10 +1600,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
|
||||
lp.sec(f"Second-time export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
path2 = ac1.export_all(backupdir.strpath)
|
||||
path2 = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path2)
|
||||
assert path2 != path
|
||||
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_ac_setup_message(acfactory, lp):
|
||||
@@ -1584,6 +1679,39 @@ def test_qr_join_chat(acfactory, lp):
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
# ac1 adds ac2 as a contact with an email address in uppercase.
|
||||
ac2_addr_uppercase = ac2.get_config("addr").upper()
|
||||
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
|
||||
ac1.create_contact(ac2_addr_uppercase)
|
||||
|
||||
lp.sec("ac3 creates a verified group with a QR code")
|
||||
chat = ac3.create_group_chat("hello", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a verified group via a QR code")
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
# ac1 should see both ac3 and ac2 as verified.
|
||||
assert len(ac1_chat.get_contacts()) == 3
|
||||
for contact in ac1_chat.get_contacts():
|
||||
assert contact.is_verified()
|
||||
|
||||
|
||||
def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
lp.sec("configuring ac1 and ac2")
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1800,15 +1928,15 @@ def test_connectivity(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test stop_io() and start_io()")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
@@ -1829,8 +1957,8 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac2.create_chat(ac1).send_text("Hi 2")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
@@ -1840,7 +1968,7 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac1.maybe_network()
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -1855,7 +1983,7 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -1864,10 +1992,10 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
@@ -2350,9 +2478,9 @@ def test_archived_muted_chat(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
|
||||
while 1:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
assert ev.data2 == 0
|
||||
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
assert archive.count_fresh_messages() == 1
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
break
|
||||
|
||||
@@ -30,28 +30,28 @@ def wait_msgs_changed(account, msgs_list):
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt").ensure(file=1)
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.touch()
|
||||
with pytest.raises(Exception):
|
||||
chat.prepare_message_file(src.strpath)
|
||||
chat.prepare_message_file(str(src))
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt")
|
||||
src.write("hello there\n")
|
||||
chat.send_file(src.strpath)
|
||||
|
||||
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.write_text("hello there\n")
|
||||
msg = chat.send_file(str(src))
|
||||
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
||||
assert msg.filename.endswith(".txt")
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -4,12 +4,11 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat import Account, const
|
||||
import deltachat as dc
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -52,18 +51,18 @@ def test_parse_system_add_remove(msgtext, res):
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
account = Account(p.strpath)
|
||||
def test_wrong_db(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
p.write_text("123")
|
||||
account = Account(str(p))
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
def test_os_name(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
# we can't easily test if os_name is used in X-Mailer
|
||||
# outgoing messages without a full Online test
|
||||
# but we at least check Account accepts the arg
|
||||
ac1 = Account(p.strpath, os_name="solarpunk")
|
||||
ac1 = Account(str(p), os_name="solarpunk")
|
||||
ac1.get_info()
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
@@ -299,13 +298,13 @@ class TestOfflineChat:
|
||||
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
@@ -481,6 +480,19 @@ class TestOfflineChat:
|
||||
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
|
||||
assert contact2.name == "real"
|
||||
|
||||
def test_send_lots_of_offline_msgs(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("configured_mail_server", "example.org")
|
||||
ac1.set_config("configured_mail_user", "example.org")
|
||||
ac1.set_config("configured_mail_pw", "example.org")
|
||||
ac1.set_config("configured_send_server", "example.org")
|
||||
ac1.set_config("configured_send_user", "example.org")
|
||||
ac1.set_config("configured_send_pw", "example.org")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
for i in range(50):
|
||||
chat.send_text("hello")
|
||||
|
||||
def test_create_chat_simple(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
@@ -496,22 +508,22 @@ class TestOfflineChat:
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path):
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
path = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
@@ -525,27 +537,27 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
passphrase2 = "passphrase2"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
path = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
@@ -580,27 +592,27 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmpdir):
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, passphrase)
|
||||
path = ac1.export_all(str(backupdir), passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
@@ -619,7 +631,7 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
Test that account passphrase isn't lost if backup failed to be imported.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/3379
|
||||
@@ -627,24 +639,24 @@ class TestOfflineChat:
|
||||
acct_passphrase = "passphrase1"
|
||||
bak_passphrase = "passphrase2"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, bak_passphrase)
|
||||
path = ac1.export_all(str(backupdir), bak_passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
@@ -805,7 +817,7 @@ class TestOfflineChat:
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref,
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from queue import Queue
|
||||
|
||||
from deltachat import capi, const, cutil, register_global_plugin
|
||||
import deltachat as dc
|
||||
from deltachat import capi, cutil, register_global_plugin
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.testplugin import (
|
||||
@@ -65,16 +65,17 @@ class TestACSetup:
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
holder = acfactory._acsetup.testprocess
|
||||
assert holder.cache_maybe_store_configured_db_files(ac1)
|
||||
assert not holder.cache_maybe_store_configured_db_files(ac1)
|
||||
acdir = tmpdir.mkdir("newaccount")
|
||||
acdir = tmp_path / "newaccount"
|
||||
acdir.mkdir()
|
||||
addr = ac1.get_config("addr")
|
||||
target_db_path = acdir.join("db").strpath
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
|
||||
assert len(os.listdir(acdir)) >= 2
|
||||
target_db_path = acdir / "db"
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
|
||||
assert sum(1 for _ in acdir.iterdir()) >= 2
|
||||
|
||||
|
||||
def test_liveconfig_caching(acfactory, monkeypatch):
|
||||
@@ -112,40 +113,40 @@ def test_dc_close_events(acfactory):
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
|
||||
def test_wrong_db(tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
def test_wrong_db(tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
p.write_bytes(b"x123" * 10)
|
||||
|
||||
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_context_is_open(context)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
def test_empty_blobdir(tmp_path):
|
||||
db_fname = tmp_path / "hello.db"
|
||||
# Apparently some client code expects this to be the same as passing NULL.
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
|
||||
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert ctx != ffi.NULL
|
||||
|
||||
|
||||
def test_event_defines():
|
||||
assert const.DC_EVENT_INFO == 100
|
||||
assert const.DC_CONTACT_ID_SELF
|
||||
assert dc.const.DC_EVENT_INFO == 100
|
||||
assert dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
|
||||
def test_sig():
|
||||
sig = capi.lib.dc_event_has_string_data
|
||||
assert not sig(const.DC_EVENT_MSGS_CHANGED)
|
||||
assert sig(const.DC_EVENT_INFO)
|
||||
assert sig(const.DC_EVENT_WARNING)
|
||||
assert sig(const.DC_EVENT_ERROR)
|
||||
assert sig(const.DC_EVENT_SMTP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_IMAP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||
assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
|
||||
assert sig(dc.const.DC_EVENT_INFO)
|
||||
assert sig(dc.const.DC_EVENT_WARNING)
|
||||
assert sig(dc.const.DC_EVENT_ERROR)
|
||||
assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
|
||||
assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
|
||||
assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||
assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
@@ -174,10 +175,10 @@ def test_provider_info_none():
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_open(tmpdir):
|
||||
db_fname = tmpdir.join("test.db")
|
||||
def test_get_info_open(tmp_path):
|
||||
db_fname = tmp_path / "test.db"
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
@@ -218,10 +219,10 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
assert "Traceback" in res
|
||||
|
||||
|
||||
def test_jsonrpc_blocking_call(tmpdir):
|
||||
accounts_fname = tmpdir.join("accounts")
|
||||
def test_jsonrpc_blocking_call(tmp_path):
|
||||
accounts_fname = tmp_path / "accounts"
|
||||
accounts = ffi.gc(
|
||||
lib.dc_accounts_new(ffi.NULL, accounts_fname.strpath.encode("ascii")),
|
||||
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-05-12
|
||||
2023-08-28
|
||||
@@ -1,3 +0,0 @@
|
||||
pre-release-commit-message = "chore({{crate_name}}): release {{version}}"
|
||||
pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}"
|
||||
no-dev-version = true
|
||||
@@ -18,6 +18,10 @@ and an own build machine.
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
|
||||
- `make-python-testenv.sh` creates or updates local python test development environment.
|
||||
Reusing the same environment is faster than running `run-python-test.sh` which always
|
||||
recreates environment from scratch and runs additional lints.
|
||||
|
||||
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
|
||||
- `run_all.sh` builds Python wheels
|
||||
|
||||
@@ -12,10 +12,15 @@ where `secret.yml` contains the following secrets:
|
||||
```
|
||||
c.delta.chat:
|
||||
private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
devpi:
|
||||
login: dc
|
||||
password: ...
|
||||
```
|
||||
|
||||
Secrets can be read from the password manager:
|
||||
```
|
||||
fly -t b1 set-pipeline -c docs_wheels.yml -p docs_wheels -l <(pass show delta/b1.delta.chat/secret.yml)
|
||||
```
|
||||
|
||||
@@ -153,11 +153,13 @@ jobs:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-aarch64
|
||||
plan:
|
||||
@@ -223,11 +225,13 @@ jobs:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-musl-x86_64
|
||||
plan:
|
||||
@@ -293,11 +297,13 @@ jobs:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
|
||||
- name: python-musl-aarch64
|
||||
plan:
|
||||
@@ -363,8 +369,10 @@ jobs:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*musllinux_1_1_aarch64*
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools python3-venv
|
||||
python3 -m venv env
|
||||
env/bin/pip install --upgrade pip
|
||||
env/bin/pip install devpi
|
||||
env/bin/devpi use https://m.devpi.net/dc/master
|
||||
env/bin/devpi login ((devpi.login)) --password ((devpi.password))
|
||||
env/bin/devpi upload py-wheels/*musllinux_1_1_aarch64*
|
||||
|
||||
@@ -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.68.0
|
||||
RUST_VERSION=1.72.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
21
scripts/make-python-testenv.sh
Executable file
21
scripts/make-python-testenv.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Script to create or update a python development environment.
|
||||
# It rebuilds the core and bindings as needed.
|
||||
#
|
||||
# After running the script, you can either
|
||||
# run `pytest` directly with `env/bin/pytest python/`
|
||||
# or activate the environment with `. env/bin/activacte`
|
||||
# and run `pytest` from there.
|
||||
set -euo pipefail
|
||||
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
if test -d env; then
|
||||
env/bin/pip install -e python --force-reinstall
|
||||
else
|
||||
tox -e py --devenv env
|
||||
env/bin/pip install --upgrade pip
|
||||
fi
|
||||
@@ -7,6 +7,7 @@ import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
|
||||
rex = re.compile(r'version = "(\S+)"')
|
||||
|
||||
@@ -95,22 +96,14 @@ def main():
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
if "alpha" not in newversion:
|
||||
changelog_name = "CHANGELOG.md"
|
||||
changelog_tmpname = changelog_name + ".tmp"
|
||||
changelog_tmp = open(changelog_tmpname, "w")
|
||||
found = False
|
||||
for line in open(changelog_name):
|
||||
## 1.25.0
|
||||
if line == f"## [{newversion}]\n":
|
||||
line = f"## [{newversion}] - {today}\n"
|
||||
for line in Path("CHANGELOG.md").open():
|
||||
if line == f"## [{newversion}] - {today}\n":
|
||||
found = True
|
||||
changelog_tmp.write(line)
|
||||
if not found:
|
||||
raise SystemExit(
|
||||
f"{changelog_name} contains no entry for version: {newversion}"
|
||||
)
|
||||
changelog_tmp.close()
|
||||
os.rename(changelog_tmpname, changelog_name)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
|
||||
28
scripts/zig-musl-check.sh
Executable file
28
scripts/zig-musl-check.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Run `cargo check` with musl libc.
|
||||
# This requires `zig` to compile vendored openssl.
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
|
||||
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
CC="$PWD/scripts/zig-cc" \
|
||||
TARGET_CC="$PWD/scripts/zig-cc" \
|
||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
|
||||
LD="$PWD/scripts/zig-cc" \
|
||||
ZIG_TARGET="x86_64-linux-musl" \
|
||||
cargo check --release --target x86_64-unknown-linux-musl -p deltachat_ffi --features jsonrpc
|
||||
@@ -8,9 +8,9 @@ set -e
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.68.1
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
|
||||
@@ -296,10 +296,10 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
const CONFIG_NAME: &str = "accounts.toml";
|
||||
|
||||
/// Database file name.
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
||||
@@ -357,7 +357,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::message;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
@@ -705,7 +704,7 @@ Authentication-Results: dkim=";
|
||||
let received = tcm
|
||||
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
|
||||
.await;
|
||||
assert!(!received.text.as_ref().unwrap().contains("1234"));
|
||||
assert!(!received.text.contains("1234"));
|
||||
assert!(received.error.is_some());
|
||||
|
||||
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
|
||||
@@ -786,7 +785,7 @@ Authentication-Results: dkim=";
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert!(!rcvd.get_showpadlock());
|
||||
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
|
||||
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -825,7 +824,9 @@ Authentication-Results: dkim=";
|
||||
// Disallowing keychanges is disabled for now:
|
||||
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
|
||||
// The message info should contain a warning:
|
||||
assert!(message::get_msg_info(&bob, rcvd.id)
|
||||
assert!(rcvd
|
||||
.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("KEYCHANGES NOT ALLOWED"));
|
||||
|
||||
118
src/blob.rs
118
src/blob.rs
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use futures::StreamExt;
|
||||
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
@@ -323,18 +323,35 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(
|
||||
context,
|
||||
blob_abs,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
20_000,
|
||||
strict_limits,
|
||||
)? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
|
||||
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
|
||||
/// height and file size specified by the config.
|
||||
///
|
||||
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
|
||||
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
|
||||
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
|
||||
/// reset.
|
||||
pub async fn recode_to_image_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
maybe_sticker: &mut bool,
|
||||
) -> Result<()> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
@@ -347,9 +364,14 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let strict_limits = false;
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(
|
||||
context,
|
||||
blob_abs,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
strict_limits,
|
||||
)? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
@@ -358,20 +380,37 @@ impl<'a> BlobObject<'a> {
|
||||
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||
/// proceed with the result.
|
||||
fn recode_to_size(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
maybe_sticker: &mut bool,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
) -> Result<Option<String>> {
|
||||
tokio::task::block_in_place(move || {
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
*no_exif_ref = exif.is_none();
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
|
||||
if *maybe_sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
*maybe_sticker = img.in_bounds(x_max, y_max)
|
||||
&& (img.get_pixel(0, 0).0[3] == 0
|
||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||
|| img.get_pixel(0, y_max).0[3] == 0
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0);
|
||||
}
|
||||
if *maybe_sticker && exif.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
Some(180) => img.rotate180(),
|
||||
@@ -469,7 +508,21 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
Ok(changed_name)
|
||||
})
|
||||
});
|
||||
match res {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !strict_limits && no_exif {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
);
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns image file size and Exif.
|
||||
@@ -860,10 +913,18 @@ mod tests {
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
blob.recode_to_size(
|
||||
&t,
|
||||
blob.to_abs_path(),
|
||||
maybe_sticker,
|
||||
1000,
|
||||
3000,
|
||||
strict_limits,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
tokio::task::block_in_place(move || {
|
||||
@@ -923,6 +984,7 @@ mod tests {
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -936,6 +998,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -955,6 +1018,7 @@ mod tests {
|
||||
// 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 = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -974,6 +1038,7 @@ mod tests {
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
&bytes,
|
||||
"jpg",
|
||||
@@ -994,6 +1059,7 @@ mod tests {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"png",
|
||||
@@ -1008,6 +1074,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"png",
|
||||
@@ -1020,12 +1087,29 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Sticker,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
.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");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -1059,6 +1143,7 @@ mod tests {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_image_check_mediaquality(
|
||||
viewtype: Viewtype,
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
extension: &str,
|
||||
@@ -1090,7 +1175,7 @@ mod tests {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
@@ -1104,6 +1189,7 @@ mod tests {
|
||||
);
|
||||
|
||||
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 = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
447
src/chat.rs
447
src/chat.rs
@@ -461,7 +461,7 @@ impl ChatId {
|
||||
promote: bool,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let msg_text = context.stock_protection_msg(protect, from_id).await;
|
||||
let text = context.stock_protection_msg(protect, from_id).await;
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
@@ -470,7 +470,7 @@ impl ChatId {
|
||||
if promote {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(msg_text),
|
||||
text,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(cmd);
|
||||
@@ -479,7 +479,7 @@ impl ChatId {
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&msg_text,
|
||||
&text,
|
||||
cmd,
|
||||
create_smeared_timestamp(context),
|
||||
None,
|
||||
@@ -642,7 +642,7 @@ impl ChatId {
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::self_deleted_msg_body(context).await);
|
||||
msg.text = stock_str::self_deleted_msg_body(context).await;
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
|
||||
@@ -724,7 +724,7 @@ impl ChatId {
|
||||
match msg.viewtype {
|
||||
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
|
||||
Viewtype::Text => {
|
||||
if msg.text.is_none_or_empty() && msg.in_reply_to.is_none_or_empty() {
|
||||
if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() {
|
||||
bail!("No text and no quote in draft");
|
||||
}
|
||||
}
|
||||
@@ -770,7 +770,7 @@ impl ChatId {
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
&msg.text,
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id,
|
||||
@@ -804,7 +804,7 @@ impl ChatId {
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
&msg.text,
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
@@ -983,7 +983,7 @@ impl ChatId {
|
||||
.iter()
|
||||
.filter(|&contact_id| !contact_id.is_special())
|
||||
{
|
||||
let contact = Contact::load_from_db(context, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
let peerstate = Peerstate::from_addr(context, addr).await?;
|
||||
|
||||
@@ -1358,7 +1358,7 @@ impl Chat {
|
||||
/// deltachat, and the data returned is still subject to change.
|
||||
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
|
||||
let draft = match self.id.get_draft(context).await? {
|
||||
Some(message) => message.text.unwrap_or_default(),
|
||||
Some(message) => message.text,
|
||||
_ => String::new(),
|
||||
};
|
||||
Ok(ChatInfo {
|
||||
@@ -1400,6 +1400,7 @@ impl Chat {
|
||||
}
|
||||
|
||||
/// Returns true if the chat is promoted.
|
||||
/// This means a message has been sent to it and it _not_ only exists on the users device.
|
||||
pub fn is_promoted(&self) -> bool {
|
||||
!self.is_unpromoted()
|
||||
}
|
||||
@@ -1603,7 +1604,7 @@ impl Chat {
|
||||
timestamp,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.text,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -1652,7 +1653,7 @@ impl Chat {
|
||||
timestamp,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.text,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -1808,7 +1809,7 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
|
||||
chat.param.set(Param::ProfileImage, &icon);
|
||||
chat.update_param(context).await?;
|
||||
|
||||
let mut contact = Contact::load_from_db(context, ContactId::DEVICE).await?;
|
||||
let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
|
||||
contact.param.set(Param::ProfileImage, icon);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
@@ -1932,7 +1933,7 @@ impl ChatIdBlocked {
|
||||
|
||||
/// Returns the chat for the 1:1 chat with this contact.
|
||||
///
|
||||
/// I the chat does not yet exist a new one is created, using the provided [`Blocked`]
|
||||
/// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
|
||||
/// state.
|
||||
pub async fn get_for_contact(
|
||||
context: &Context,
|
||||
@@ -1950,7 +1951,7 @@ impl ChatIdBlocked {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let chat_name = contact.get_display_name().to_string();
|
||||
let mut params = Params::new();
|
||||
match contact_id {
|
||||
@@ -2032,15 +2033,23 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
if let Err(err) = blob.recode_to_image_size(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}."
|
||||
);
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image || maybe_sticker {
|
||||
blob.recode_to_image_size(context, &mut maybe_sticker)
|
||||
.await?;
|
||||
if !maybe_sticker {
|
||||
msg.viewtype = Viewtype::Image;
|
||||
}
|
||||
}
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
|
||||
let stem = match filename.rsplit_once('.') {
|
||||
Some((stem, _)) => stem,
|
||||
None => filename,
|
||||
};
|
||||
msg.param
|
||||
.set(Param::Filename, stem.to_string() + "." + blob_ext);
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
@@ -2203,9 +2212,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// protect all system messages againts RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
if let Some(text) = &msg.text {
|
||||
msg.text = Some(strip_rtlo_characters(text.as_ref()));
|
||||
}
|
||||
msg.text = strip_rtlo_characters(&msg.text);
|
||||
}
|
||||
|
||||
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
|
||||
@@ -2396,7 +2403,7 @@ pub async fn send_text_msg(
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(text_to_send);
|
||||
msg.text = text_to_send;
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
@@ -2422,10 +2429,9 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
msg.text = Some(
|
||||
msg.text =
|
||||
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
|
||||
.await,
|
||||
);
|
||||
.await;
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
@@ -2969,6 +2975,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
/// If the group is promoted, also sends out a system message to all group members
|
||||
pub async fn add_contact_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -2990,7 +2997,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
|
||||
chat_id.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
/*this also makes sure, not contacts are added to special or normal chats*/
|
||||
// this also makes sure, no contacts are added to special or normal chats
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
||||
@@ -3012,7 +3019,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into(),
|
||||
));
|
||||
bail!("can not add contact because our account is not part of it");
|
||||
bail!("can not add contact because the account is not part of the group/broadcast");
|
||||
}
|
||||
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
@@ -3055,10 +3062,10 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
msg.text =
|
||||
Some(stock_str::msg_add_member(context, contact.get_addr(), ContactId::SELF).await);
|
||||
let contact_addr = contact.get_addr();
|
||||
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
}
|
||||
@@ -3178,59 +3185,53 @@ pub async fn remove_contact_from_chat(
|
||||
);
|
||||
|
||||
let mut msg = Message::default();
|
||||
let mut success = false;
|
||||
|
||||
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
|
||||
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
|
||||
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot remove contact from chat; self not in group.".into(),
|
||||
));
|
||||
} else {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact.id == ContactId::SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
msg.text =
|
||||
Some(stock_str::msg_group_left(context, ContactId::SELF).await);
|
||||
} else {
|
||||
msg.text = Some(
|
||||
stock_str::msg_del_member(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
let err_msg = format!(
|
||||
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
|
||||
bail!("{}", err_msg);
|
||||
} else {
|
||||
// We do not return an error if the contact does not exist in the database.
|
||||
// This allows to delete dangling references to deleted contacts
|
||||
// in case of the database becoming inconsistent due to a bug.
|
||||
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact.id == ContactId::SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
// to-be-send message. If between send_msg() and here the
|
||||
// process dies the user will have to re-do the action. It's
|
||||
// better than the other way round: you removed
|
||||
// someone from DB but no peer or device gets to know about it and
|
||||
// group membership is thus different on different devices.
|
||||
// Note also that sending a message needs all recipients
|
||||
// in order to correctly determine encryption so if we
|
||||
// removed it first, it would complicate the
|
||||
// check/encryption logic.
|
||||
success = remove_from_chat_contacts_table(context, chat_id, contact_id)
|
||||
.await
|
||||
.is_ok();
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
// to-be-send message. If between send_msg() and here the
|
||||
// process dies the user will have to re-do the action. It's
|
||||
// better than the other way round: you removed
|
||||
// someone from DB but no peer or device gets to know about it and
|
||||
// group membership is thus different on different devices.
|
||||
// Note also that sending a message needs all recipients
|
||||
// in order to correctly determine encryption so if we
|
||||
// removed it first, it would complicate the
|
||||
// check/encryption logic.
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
bail!("Failed to remove contact");
|
||||
} else {
|
||||
bail!("Cannot remove members from non-group chats.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3291,9 +3292,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
&& improve_single_line_input(&chat.name) != new_name
|
||||
{
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await,
|
||||
);
|
||||
msg.text =
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
|
||||
msg.param.set_cmd(SystemMessage::GroupNameChanged);
|
||||
if !chat.name.is_empty() {
|
||||
msg.param.set(Param::Arg, &chat.name);
|
||||
@@ -3342,13 +3342,13 @@ pub async fn set_chat_profile_image(
|
||||
if new_image.is_empty() {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
msg.param.remove(Param::Arg);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
|
||||
msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await;
|
||||
} else {
|
||||
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
|
||||
image_blob.recode_to_avatar_size(context).await?;
|
||||
chat.param.set(Param::ProfileImage, image_blob.as_name());
|
||||
msg.param.set(Param::Arg, image_blob.as_name());
|
||||
msg.text = Some(stock_str::msg_grp_img_changed(context, ContactId::SELF).await);
|
||||
msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
|
||||
}
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
@@ -3623,7 +3623,7 @@ pub async fn add_device_msg_with_importance(
|
||||
timestamp_sent, // timestamp_sent equals timestamp_rcvd
|
||||
msg.viewtype,
|
||||
state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
&msg.text,
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
),
|
||||
@@ -3797,7 +3797,7 @@ mod tests {
|
||||
use crate::contact::{Contact, ContactAddress};
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use tokio::fs;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -3860,7 +3860,7 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
msg.set_text("hello".to_string());
|
||||
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
|
||||
@@ -3875,12 +3875,12 @@ mod tests {
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi!".to_string()));
|
||||
msg.set_text("hi!".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("another".to_string()));
|
||||
msg.set_text("another".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
@@ -3895,7 +3895,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
msg.set_text("hello".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
@@ -3909,7 +3909,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
msg.set_text("hello".to_string());
|
||||
assert_eq!(msg.id, MsgId::new_unset());
|
||||
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
|
||||
|
||||
@@ -3922,7 +3922,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(id_after_1st_set, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
msg.set_text(Some("hello2".to_string()));
|
||||
msg.set_text("hello2".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
let id_after_2nd_set = msg.id;
|
||||
|
||||
@@ -3934,7 +3934,7 @@ mod tests {
|
||||
let test = chat_id.get_draft(&t).await?.unwrap();
|
||||
assert_eq!(id_after_2nd_set, test.id);
|
||||
assert_eq!(id_after_2nd_set, msg.id);
|
||||
assert_eq!(test.text, Some("hello2".to_string()));
|
||||
assert_eq!(test.text, "hello2".to_string());
|
||||
assert_eq!(test.state, MessageState::OutDraft);
|
||||
|
||||
let id_after_prepare = prepare_msg(&t, *chat_id, &mut msg).await?;
|
||||
@@ -3962,11 +3962,11 @@ mod tests {
|
||||
|
||||
// save a draft
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(Some("draft text".to_string()));
|
||||
draft.set_text("draft text".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("draft text".to_string()));
|
||||
assert_eq!(test.text, "draft text".to_string());
|
||||
assert!(test.quoted_text().is_none());
|
||||
assert!(test.quoted_message(&t).await?.is_none());
|
||||
|
||||
@@ -3975,17 +3975,17 @@ mod tests {
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("draft text".to_string()));
|
||||
assert_eq!(test.text, "draft text".to_string());
|
||||
assert_eq!(test.quoted_text(), Some("quote1".to_string()));
|
||||
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id);
|
||||
|
||||
// change quote on same message object
|
||||
draft.set_text(Some("another draft text".to_string()));
|
||||
draft.set_text("another draft text".to_string());
|
||||
draft.set_quote(&t, Some("e2)).await?;
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("another draft text".to_string()));
|
||||
assert_eq!(test.text, "another draft text".to_string());
|
||||
assert_eq!(test.quoted_text(), Some("quote2".to_string()));
|
||||
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id);
|
||||
|
||||
@@ -3994,7 +3994,7 @@ mod tests {
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("another draft text".to_string()));
|
||||
assert_eq!(test.text, "another draft text".to_string());
|
||||
assert!(test.quoted_text().is_none());
|
||||
assert!(test.quoted_message(&t).await?.is_none());
|
||||
|
||||
@@ -4014,6 +4014,144 @@ mod tests {
|
||||
assert_eq!(added, false);
|
||||
}
|
||||
|
||||
/// Test adding and removing members in a group chat.
|
||||
///
|
||||
/// Make sure messages sent outside contain authname
|
||||
/// and displayed messages contain locally set name.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_member_add_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Disable encryption so we can inspect raw message contents.
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
|
||||
// Create contact for Bob on the Alice side with name "robert".
|
||||
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
|
||||
|
||||
// Set Bob authname to "Bob" and send it to Alice.
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
tcm.send_recv(&bob, &alice, "Hello!").await;
|
||||
|
||||
// Check that Alice has Bob's name set to "robert" and authname set to "Bob".
|
||||
{
|
||||
let alice_bob_contact = Contact::get_by_id(&alice, alice_bob_contact_id).await?;
|
||||
assert_eq!(alice_bob_contact.get_name(), "robert");
|
||||
|
||||
// This is the name that will be sent outside.
|
||||
assert_eq!(alice_bob_contact.get_authname(), "Bob");
|
||||
|
||||
assert_eq!(alice_bob_contact.get_display_name(), "robert");
|
||||
}
|
||||
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let sent = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
assert!(sent.payload.contains("Hi! I created a group."));
|
||||
|
||||
// Alice adds Bob to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I added member Bob (bob@example.net)."));
|
||||
// Locally set name "robert" should not leak.
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
"You added member robert (bob@example.net)."
|
||||
);
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I removed member Bob (bob@example.net)."));
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
"You removed member robert (bob@example.net)."
|
||||
);
|
||||
|
||||
// Alice leaves the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("I left the group."));
|
||||
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test simultaneous removal of user from the chat and leaving the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_simultaneous_member_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_claire_contact_id =
|
||||
Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
let alice_sent_msg = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
let bob_received_msg = bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Alice adds Claire to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
// Test that add message is rewritten.
|
||||
assert_eq!(
|
||||
bob_received_add_msg.get_text(),
|
||||
"Member claire@example.net added by alice@example.org."
|
||||
);
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
|
||||
// Test that remove message is rewritten.
|
||||
assert_eq!(
|
||||
bob_received_remove_msg.get_text(),
|
||||
"Member Me (bob@example.net) removed by alice@example.org."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
let a1 = TestContext::new_alice().await;
|
||||
@@ -4210,6 +4348,7 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that adding or removing contacts in 1:1 chat is not allowed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_remove_contact_for_single() {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
@@ -4257,7 +4396,7 @@ mod tests {
|
||||
t2.recv_msg(&sent_msg).await;
|
||||
let chat = &t2.get_self_chat().await;
|
||||
let msg = t2.get_last_msg_in(chat.id).await;
|
||||
assert_eq!(msg.text, Some("foo self".to_string()));
|
||||
assert_eq!(msg.text, "foo self".to_string());
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert_eq!(msg.to_id, ContactId::SELF);
|
||||
assert!(msg.get_showpadlock());
|
||||
@@ -4271,12 +4410,12 @@ mod tests {
|
||||
|
||||
// add two device-messages
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.text = Some("first message".to_string());
|
||||
msg1.set_text("first message".to_string());
|
||||
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
|
||||
assert!(msg1_id.is_ok());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.text = Some("second message".to_string());
|
||||
msg2.set_text("second message".to_string());
|
||||
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
|
||||
assert!(msg2_id.is_ok());
|
||||
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
|
||||
@@ -4285,7 +4424,7 @@ mod tests {
|
||||
let msg1 = message::Message::load_from_db(&t, msg1_id.unwrap()).await;
|
||||
assert!(msg1.is_ok());
|
||||
let msg1 = msg1.unwrap();
|
||||
assert_eq!(msg1.text.as_ref().unwrap(), "first message");
|
||||
assert_eq!(msg1.text, "first message");
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
@@ -4294,7 +4433,7 @@ mod tests {
|
||||
let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await;
|
||||
assert!(msg2.is_ok());
|
||||
let msg2 = msg2.unwrap();
|
||||
assert_eq!(msg2.text.as_ref().unwrap(), "second message");
|
||||
assert_eq!(msg2.text, "second message");
|
||||
|
||||
// check device chat
|
||||
assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2);
|
||||
@@ -4306,13 +4445,13 @@ mod tests {
|
||||
|
||||
// add two device-messages with the same label (second attempt is not added)
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.text = Some("first message".to_string());
|
||||
msg1.text = "first message".to_string();
|
||||
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
|
||||
assert!(msg1_id.is_ok());
|
||||
assert!(!msg1_id.as_ref().unwrap().is_unset());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.text = Some("second message".to_string());
|
||||
msg2.text = "second message".to_string();
|
||||
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
|
||||
assert!(msg2_id.is_ok());
|
||||
assert!(msg2_id.as_ref().unwrap().is_unset());
|
||||
@@ -4320,7 +4459,7 @@ mod tests {
|
||||
// check added message
|
||||
let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await?;
|
||||
assert_eq!(msg1_id.as_ref().unwrap(), &msg1.id);
|
||||
assert_eq!(msg1.text.as_ref().unwrap(), "first message");
|
||||
assert_eq!(msg1.text, "first message");
|
||||
assert_eq!(msg1.from_id, ContactId::DEVICE);
|
||||
assert_eq!(msg1.to_id, ContactId::SELF);
|
||||
assert!(!msg1.is_info());
|
||||
@@ -4360,7 +4499,7 @@ mod tests {
|
||||
assert!(res.is_ok());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
msg.set_text("message text".to_string());
|
||||
|
||||
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
|
||||
assert!(msg_id.is_ok());
|
||||
@@ -4378,7 +4517,7 @@ mod tests {
|
||||
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
msg.set_text("message text".to_string());
|
||||
add_device_msg(&t, Some("another-label"), Some(&mut msg))
|
||||
.await
|
||||
.ok();
|
||||
@@ -4396,7 +4535,7 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
msg.set_text("message text".to_string());
|
||||
add_device_msg(&t, Some("some-label"), Some(&mut msg))
|
||||
.await
|
||||
.ok();
|
||||
@@ -4420,7 +4559,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
msg.set_text("message text".to_string());
|
||||
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
|
||||
@@ -4432,7 +4571,7 @@ mod tests {
|
||||
async fn test_delete_and_reset_all_device_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
msg.set_text("message text".to_string());
|
||||
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4465,7 +4604,7 @@ mod tests {
|
||||
// create two chats
|
||||
let t = TestContext::new().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("foo".to_string());
|
||||
msg.set_text("foo".to_string());
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t, msg_id)
|
||||
.await
|
||||
@@ -4746,7 +4885,7 @@ mod tests {
|
||||
|
||||
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("foo".to_string());
|
||||
msg.set_text("foo".to_string());
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t, msg_id)
|
||||
.await
|
||||
@@ -4824,7 +4963,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi!".into()));
|
||||
msg.set_text("hi!".into());
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
@@ -4962,7 +5101,7 @@ mod tests {
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Text);
|
||||
assert_eq!(msg.get_text().unwrap(), "foo info");
|
||||
assert_eq!(msg.get_text(), "foo info");
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
|
||||
assert!(msg.parent(&t).await?.is_none());
|
||||
@@ -4989,7 +5128,7 @@ mod tests {
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Text);
|
||||
assert_eq!(msg.get_text().unwrap(), "foo bar info");
|
||||
assert_eq!(msg.get_text(), "foo bar info");
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
|
||||
assert!(msg.parent(&t).await?.is_none());
|
||||
@@ -5147,9 +5286,11 @@ mod tests {
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Gr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 1);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Gr.").count(), 1);
|
||||
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
@@ -5174,7 +5315,7 @@ mod tests {
|
||||
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(msg.text, Some("ho!".to_string()));
|
||||
assert_eq!(msg.text, "ho!".to_string());
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
@@ -5369,7 +5510,13 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
async fn test_sticker(
|
||||
filename: &str,
|
||||
bytes: &[u8],
|
||||
res_viewtype: Viewtype,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
@@ -5383,12 +5530,19 @@ mod tests {
|
||||
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let mime = sent_msg.payload();
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
if res_viewtype == Viewtype::Sticker {
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
}
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_filename(), Some(filename.to_string()));
|
||||
assert_eq!(msg.get_viewtype(), res_viewtype);
|
||||
let msg_filename = msg.get_filename().unwrap();
|
||||
match res_viewtype {
|
||||
Viewtype::Sticker => assert_eq!(msg_filename, filename),
|
||||
Viewtype::Image => assert!(msg_filename.starts_with("image_")),
|
||||
_ => panic!("Not implemented"),
|
||||
}
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
|
||||
@@ -5400,9 +5554,10 @@ mod tests {
|
||||
async fn test_sticker_png() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
64,
|
||||
64,
|
||||
include_bytes!("../test-data/image/logo.png"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -5412,6 +5567,7 @@ mod tests {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||
Viewtype::Image,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
@@ -5422,9 +5578,10 @@ mod tests {
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../test-data/image/image100x50.gif"),
|
||||
100,
|
||||
50,
|
||||
include_bytes!("../test-data/image/logo.gif"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -5438,8 +5595,8 @@ mod tests {
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// create sticker
|
||||
let file_name = "sticker.jpg";
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
let file_name = "sticker.png";
|
||||
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
@@ -5468,7 +5625,7 @@ mod tests {
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
msg.set_text("Hi Bob".to_owned());
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
@@ -5476,7 +5633,7 @@ mod tests {
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
let msg = alice.recv_msg(&forwarded_msg).await;
|
||||
assert!(msg.get_text().unwrap() == "Hi Bob");
|
||||
assert_eq!(msg.get_text(), "Hi Bob");
|
||||
assert!(msg.is_forwarded());
|
||||
Ok(())
|
||||
}
|
||||
@@ -5491,7 +5648,7 @@ mod tests {
|
||||
add_contact_to_chat(&t, chat_id1, bob_id).await?;
|
||||
let msg1 = t.get_last_msg_in(chat_id1).await;
|
||||
assert!(msg1.is_info());
|
||||
assert!(msg1.get_text().unwrap().contains("bob@example.net"));
|
||||
assert!(msg1.get_text().contains("bob@example.net"));
|
||||
|
||||
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
|
||||
@@ -5501,7 +5658,7 @@ mod tests {
|
||||
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
|
||||
assert_ne!(msg2.from_id, ContactId::INFO);
|
||||
assert_ne!(msg2.to_id, ContactId::INFO);
|
||||
assert_eq!(msg2.get_text().unwrap(), msg1.get_text().unwrap());
|
||||
assert_eq!(msg2.get_text(), msg1.get_text());
|
||||
assert!(msg2.is_forwarded());
|
||||
|
||||
Ok(())
|
||||
@@ -5520,7 +5677,7 @@ mod tests {
|
||||
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text(Some("Reply".to_owned()));
|
||||
reply.set_text("Reply".to_owned());
|
||||
reply.set_quote(&bob, Some(&received_msg)).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
let received_reply = alice.recv_msg(&sent_reply).await;
|
||||
@@ -5571,13 +5728,13 @@ mod tests {
|
||||
// Alice sends a message to Bob.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hi Bob".to_string()));
|
||||
assert_eq!(received_msg.get_text(), "Hi Bob");
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Alice sends another message to Bob, this has first message as a parent.
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.get_text(), Some("Hello Bob".to_string()));
|
||||
assert_eq!(received_msg.get_text(), "Hello Bob");
|
||||
assert_eq!(received_msg.chat_id, bob_chat.id);
|
||||
|
||||
// Bob forwards message to a group chat with Alice.
|
||||
@@ -5604,7 +5761,7 @@ mod tests {
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
|
||||
add_contact_to_chat(&alice, group_id, bob_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("bla foo".to_owned()));
|
||||
msg.set_text("bla foo".to_owned());
|
||||
let sent_msg = alice.send_msg(group_id, &mut msg).await;
|
||||
assert!(sent_msg.payload().contains("secretgrpname"));
|
||||
assert!(sent_msg.payload().contains("secretname"));
|
||||
@@ -5661,7 +5818,7 @@ mod tests {
|
||||
// Bob receives all messages
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
@@ -5678,7 +5835,7 @@ mod tests {
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
let msg = claire.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
@@ -5826,7 +5983,7 @@ mod tests {
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), Some("ola!".to_string()));
|
||||
assert_eq!(msg.get_text(), "ola!");
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
@@ -5976,7 +6133,7 @@ mod tests {
|
||||
chat_id1,
|
||||
Viewtype::Sticker,
|
||||
"b.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
include_bytes!("../test-data/image/logo.png"),
|
||||
)
|
||||
.await?;
|
||||
let second_image_msg_id = send_media(
|
||||
|
||||
@@ -319,7 +319,7 @@ impl Chatlist {
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
@@ -424,7 +424,7 @@ mod tests {
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
msg.set_text("hello".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -636,7 +636,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
msg.set_text("foo:\nbar \r\n test".to_string());
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -145,7 +145,7 @@ pub enum Config {
|
||||
/// If set to "1", on the first time `start_io()` is called after configuring,
|
||||
/// the newest existing messages are fetched.
|
||||
/// Existing recipients are added to the contact database regardless of this setting.
|
||||
#[strum(props(default = "1"))]
|
||||
#[strum(props(default = "0"))]
|
||||
FetchExistingMsgs,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
|
||||
@@ -131,7 +131,7 @@ async fn on_configure_completed(
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
for def in config_defaults {
|
||||
if !context.config_exists(def.key).await? {
|
||||
info!(context, "apply config_defaults {}={}", def.key, def.value);
|
||||
context.set_config(def.key, Some(def.value)).await?;
|
||||
@@ -146,7 +146,7 @@ async fn on_configure_completed(
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
msg.text = provider.after_login_hint.to_string();
|
||||
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
@@ -161,7 +161,7 @@ async fn on_configure_completed(
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text =
|
||||
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.context("Cannot add AEAP explanation")
|
||||
@@ -318,7 +318,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
|
||||
// respect certificate setting from function parameters
|
||||
for mut server in &mut servers {
|
||||
for server in &mut servers {
|
||||
let certificate_checks = match server.protocol {
|
||||
Protocol::Imap => param.imap.certificate_checks,
|
||||
Protocol::Smtp => param.smtp.certificate_checks,
|
||||
@@ -462,9 +462,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(¶m.addr) {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
job::schedule_resync(ctx).await?;
|
||||
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
|
||||
if configured_addr != param.addr {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
info!(ctx, "Scheduling resync because the address has changed.");
|
||||
job::schedule_resync(ctx).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// the trailing underscore is correct
|
||||
@@ -653,7 +656,7 @@ async fn try_smtp_one_param(
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
smtp.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
|
||||
let res = moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.chain(moz_ac.outgoing_servers.into_iter())
|
||||
.chain(moz_ac.outgoing_servers)
|
||||
.filter_map(|server| {
|
||||
let protocol = match server.typ.as_ref() {
|
||||
"imap" => Some(Protocol::Imap),
|
||||
|
||||
183
src/contact.rs
183
src/contact.rs
@@ -338,11 +338,33 @@ impl Default for VerifiedStatus {
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
/// Loads a contact snapshot from the database.
|
||||
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
let mut contact = context
|
||||
/// Loads a single contact object from the database.
|
||||
///
|
||||
/// Returns an error if the contact does not exist.
|
||||
///
|
||||
/// For contact ContactId::SELF (1), the function returns sth.
|
||||
/// like "Me" in the selected language and the email address
|
||||
/// defined by set_config().
|
||||
///
|
||||
/// For contact ContactId::DEVICE, the function overrides
|
||||
/// the contact name and status with localized address.
|
||||
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
let contact = Self::get_by_id_optional(context, contact_id)
|
||||
.await?
|
||||
.with_context(|| format!("contact {contact_id} not found"))?;
|
||||
Ok(contact)
|
||||
}
|
||||
|
||||
/// Loads a single contact object from the database.
|
||||
///
|
||||
/// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist.
|
||||
pub async fn get_by_id_optional(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) -> Result<Option<Self>> {
|
||||
if let Some(mut contact) = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
@@ -371,23 +393,27 @@ impl Contact {
|
||||
Ok(contact)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
} else if contact_id == ContactId::DEVICE {
|
||||
contact.name = stock_str::device_messages(context).await;
|
||||
contact.addr = ContactId::DEVICE_ADDR.to_string();
|
||||
contact.status = stock_str::device_messages_hint(context).await;
|
||||
.await?
|
||||
{
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
} else if contact_id == ContactId::DEVICE {
|
||||
contact.name = stock_str::device_messages(context).await;
|
||||
contact.addr = ContactId::DEVICE_ADDR.to_string();
|
||||
contact.status = stock_str::device_messages_hint(context).await;
|
||||
}
|
||||
Ok(Some(contact))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Ok(contact)
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact is blocked.
|
||||
@@ -407,7 +433,13 @@ impl Contact {
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
let blocked = context
|
||||
.sql
|
||||
.query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| {
|
||||
let blocked: bool = row.get(0)?;
|
||||
Ok(blocked)
|
||||
})
|
||||
.await?;
|
||||
Ok(blocked)
|
||||
}
|
||||
|
||||
@@ -959,7 +991,7 @@ impl Contact {
|
||||
);
|
||||
|
||||
let mut ret = String::new();
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
@@ -1046,17 +1078,6 @@ impl Contact {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a single contact object. For a list, see eg. get_contacts().
|
||||
///
|
||||
/// For contact ContactId::SELF (1), the function returns sth.
|
||||
/// like "Me" in the selected language and the email address
|
||||
/// defined by set_config().
|
||||
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
|
||||
Ok(contact)
|
||||
}
|
||||
|
||||
/// Updates `param` column in the database.
|
||||
pub async fn update_param(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
@@ -1120,11 +1141,29 @@ impl Contact {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Get a summary of authorized name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
/// "email@domain.com" if the name is unset.
|
||||
///
|
||||
/// This string is suitable for sending over email
|
||||
/// as it does not leak the locally set name.
|
||||
pub fn get_authname_n_addr(&self) -> String {
|
||||
if !self.authname.is_empty() {
|
||||
format!("{} ({})", self.authname, self.addr)
|
||||
} else {
|
||||
(&self.addr).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a summary of name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
/// "email@domain.com" if the name is unset.
|
||||
///
|
||||
/// The result should only be used locally and never sent over the network
|
||||
/// as it leaks the local contact name.
|
||||
///
|
||||
/// The summary is typically used when asking the user something about the contact.
|
||||
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
pub fn get_name_n_addr(&self) -> String {
|
||||
@@ -1317,7 +1356,7 @@ async fn set_block_contact(
|
||||
contact_id
|
||||
);
|
||||
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.blocked != new_blocking {
|
||||
context
|
||||
@@ -1379,7 +1418,7 @@ pub(crate) async fn set_profile_image(
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let changed = match profile_image {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
if contact_id == ContactId::SELF {
|
||||
@@ -1434,7 +1473,7 @@ pub(crate) async fn set_status(
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.status != status {
|
||||
contact.status = status;
|
||||
@@ -1752,7 +1791,7 @@ mod tests {
|
||||
.await?;
|
||||
assert_ne!(id, ContactId::UNDEFINED);
|
||||
|
||||
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_display_name(), "bob");
|
||||
@@ -1780,7 +1819,7 @@ mod tests {
|
||||
.await?;
|
||||
assert_eq!(contact_bob_id, id);
|
||||
assert_eq!(modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "someone");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_display_name(), "someone");
|
||||
@@ -1846,7 +1885,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_id(), contact_id);
|
||||
assert_eq!(contact.get_name(), "Name one");
|
||||
assert_eq!(contact.get_authname(), "bla foo");
|
||||
@@ -1865,7 +1904,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "Real one");
|
||||
assert_eq!(contact.get_addr(), "one@eins.org");
|
||||
assert!(!contact.is_blocked());
|
||||
@@ -1881,7 +1920,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::None);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "three@drei.sam");
|
||||
assert_eq!(contact.get_addr(), "three@drei.sam");
|
||||
@@ -1898,7 +1937,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
@@ -1913,7 +1952,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "m. serious");
|
||||
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
@@ -1929,14 +1968,14 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::None);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_addr(), "alice@w.de");
|
||||
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
|
||||
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
|
||||
assert_eq!(contact.get_addr(), ""); // we're not configured
|
||||
assert!(!contact.is_blocked());
|
||||
@@ -1967,7 +2006,7 @@ mod tests {
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contacts = get_chat_contacts(&t, chat_id).await?;
|
||||
let contact_id = contacts.first().unwrap();
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "f@example.org");
|
||||
@@ -1993,7 +2032,7 @@ mod tests {
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Flobbyfoo");
|
||||
@@ -2023,7 +2062,7 @@ mod tests {
|
||||
assert_eq!(chatlist.len(), 0);
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Foo Flobby");
|
||||
@@ -2041,7 +2080,7 @@ mod tests {
|
||||
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk");
|
||||
let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?;
|
||||
assert_eq!(chatlist.len(), 1);
|
||||
let contact = Contact::load_from_db(&t, *contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, *contact_id).await?;
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "Falk");
|
||||
assert_eq!(contact.get_display_name(), "Falk");
|
||||
@@ -2080,7 +2119,7 @@ mod tests {
|
||||
|
||||
// If a contact has ongoing chats, contact is only hidden on deletion
|
||||
Contact::delete(&alice, contact_id).await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert_eq!(contact.origin, Origin::Hidden);
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
@@ -2094,7 +2133,7 @@ mod tests {
|
||||
|
||||
// Can delete contact physically now
|
||||
Contact::delete(&alice, contact_id).await?;
|
||||
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
|
||||
assert!(Contact::get_by_id(&alice, contact_id).await.is_err());
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
@@ -2113,7 +2152,7 @@ mod tests {
|
||||
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
Contact::delete(&t, contact_id1).await?;
|
||||
assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
|
||||
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
|
||||
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
assert_ne!(contact_id2, contact_id1);
|
||||
@@ -2122,12 +2161,12 @@ mod tests {
|
||||
// test recreation after hiding
|
||||
t.create_chat_with_contact("Foo", "foo@bar.de").await;
|
||||
Contact::delete(&t, contact_id2).await?;
|
||||
let contact = Contact::load_from_db(&t, contact_id2).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id2).await?;
|
||||
assert_eq!(contact.origin, Origin::Hidden);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
|
||||
|
||||
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
let contact = Contact::load_from_db(&t, contact_id3).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id3).await?;
|
||||
assert_eq!(contact.origin, Origin::ManuallyCreated);
|
||||
assert_eq!(contact_id3, contact_id2);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
@@ -2150,7 +2189,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Created);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "bob1");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "bob1");
|
||||
@@ -2166,7 +2205,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "bob2");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "bob2");
|
||||
@@ -2176,7 +2215,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "bob2");
|
||||
assert_eq!(contact.get_name(), "bob3");
|
||||
assert_eq!(contact.get_display_name(), "bob3");
|
||||
@@ -2192,7 +2231,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "bob4");
|
||||
assert_eq!(contact.get_name(), "bob3");
|
||||
assert_eq!(contact.get_display_name(), "bob3");
|
||||
@@ -2205,7 +2244,7 @@ mod tests {
|
||||
// manually create "claire@example.org" without a given name
|
||||
let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap();
|
||||
assert!(!contact_id.is_special());
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "claire@example.org");
|
||||
@@ -2221,7 +2260,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "claire1");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "claire1");
|
||||
@@ -2237,7 +2276,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "claire2");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "claire2");
|
||||
@@ -2260,7 +2299,7 @@ mod tests {
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(sth_modified, Modifier::Created);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id).await?;
|
||||
assert_eq!(contact.get_display_name(), "Bob");
|
||||
|
||||
// Incoming message from someone else with "Not Bob" <bob@example.org> in the "To:" field.
|
||||
@@ -2273,7 +2312,7 @@ mod tests {
|
||||
.await?;
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id).await?;
|
||||
assert_eq!(contact.get_display_name(), "Not Bob");
|
||||
|
||||
// Incoming message from Bob, changing the name back.
|
||||
@@ -2286,7 +2325,7 @@ mod tests {
|
||||
.await?;
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
|
||||
let contact = Contact::load_from_db(&t, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&t, contact_id).await?;
|
||||
assert_eq!(contact.get_display_name(), "Bob");
|
||||
|
||||
Ok(())
|
||||
@@ -2300,7 +2339,7 @@ mod tests {
|
||||
let contact_id = Contact::create(&t, "dave1", "dave@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "dave1");
|
||||
assert_eq!(contact.get_display_name(), "dave1");
|
||||
@@ -2314,14 +2353,14 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "dave2");
|
||||
assert_eq!(contact.get_name(), "dave1");
|
||||
assert_eq!(contact.get_display_name(), "dave1");
|
||||
|
||||
// manually clear the name
|
||||
Contact::create(&t, "", "dave@example.org").await.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "dave2");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "dave2");
|
||||
@@ -2339,21 +2378,21 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let contact_id = Contact::create(&t, "", "<dave@example.org>").await.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_addr(), "dave@example.org");
|
||||
|
||||
let contact_id = Contact::create(&t, "", "Mueller, Dave <dave@example.org>")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "Mueller, Dave");
|
||||
assert_eq!(contact.get_addr(), "dave@example.org");
|
||||
|
||||
let contact_id = Contact::create(&t, "name1", "name2 <dave@example.org>")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "name1");
|
||||
assert_eq!(contact.get_addr(), "dave@example.org");
|
||||
|
||||
@@ -2597,7 +2636,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
@@ -2614,7 +2653,7 @@ Hi."#;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1042,7 +1042,7 @@ mod tests {
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::load_from_db(t, *members.first().unwrap())
|
||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
@@ -1304,11 +1304,11 @@ mod tests {
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_text(Some("foobar".to_string()));
|
||||
msg1.set_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_text(Some("barbaz".to_string()));
|
||||
msg2.set_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
@@ -1404,7 +1404,7 @@ mod tests {
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foobar".to_string()));
|
||||
msg.set_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ mod tests {
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
|
||||
assert_eq!(msg.text, "Hello from Thunderbird!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
212
src/dehtml.rs
212
src/dehtml.rs
@@ -10,10 +10,11 @@ use quick_xml::{
|
||||
Reader,
|
||||
};
|
||||
|
||||
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
use crate::simplify::{simplify_quote, SimplifiedText};
|
||||
|
||||
struct Dehtml {
|
||||
strbuilder: String,
|
||||
quote: String,
|
||||
add_text: AddText,
|
||||
last_href: Option<String>,
|
||||
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
|
||||
@@ -29,17 +30,22 @@ struct Dehtml {
|
||||
}
|
||||
|
||||
impl Dehtml {
|
||||
fn line_prefix(&self) -> &str {
|
||||
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
|
||||
"> "
|
||||
/// Returns true if HTML parser is currently inside the quote.
|
||||
fn is_quote(&self) -> bool {
|
||||
self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0
|
||||
}
|
||||
|
||||
/// Returns the buffer where the text should be written.
|
||||
///
|
||||
/// If the parser is inside the quote, returns the quote buffer.
|
||||
fn get_buf(&mut self) -> &mut String {
|
||||
if self.is_quote() {
|
||||
&mut self.quote
|
||||
} else {
|
||||
""
|
||||
&mut self.strbuilder
|
||||
}
|
||||
}
|
||||
fn append_prefix(&self, line_end: &str) -> String {
|
||||
// line_end is e.g. "\n\n". We add "> " if necessary.
|
||||
line_end.to_string() + self.line_prefix()
|
||||
}
|
||||
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
AddText::No // Everything between `<div name="quoted">` and `<div name="quoted_content">` is metadata which we don't want
|
||||
@@ -51,30 +57,70 @@ impl Dehtml {
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum AddText {
|
||||
/// Inside `<script>`, `<style>` and similar tags
|
||||
/// which contents should not be displayed.
|
||||
No,
|
||||
|
||||
YesRemoveLineEnds,
|
||||
|
||||
/// Inside `<pre>`.
|
||||
YesPreserveLineEnds,
|
||||
}
|
||||
|
||||
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
|
||||
// the newlines are typically removed in further processing by the caller
|
||||
pub fn dehtml(buf: &str) -> Option<String> {
|
||||
let s = dehtml_quick_xml(buf);
|
||||
pub(crate) fn dehtml(buf: &str) -> Option<SimplifiedText> {
|
||||
let (s, quote) = dehtml_quick_xml(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return Some(s);
|
||||
let text = dehtml_cleanup(s);
|
||||
let top_quote = if !quote.trim().is_empty() {
|
||||
Some(dehtml_cleanup(simplify_quote("e).0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Some(SimplifiedText {
|
||||
text,
|
||||
top_quote,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let s = dehtml_manually(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return Some(s);
|
||||
let text = dehtml_cleanup(s);
|
||||
return Some(SimplifiedText {
|
||||
text,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn dehtml_quick_xml(buf: &str) -> String {
|
||||
fn dehtml_cleanup(mut text: String) -> String {
|
||||
text.retain(|c| c != '\r');
|
||||
let lines = text.trim().split('\n');
|
||||
let mut text = String::new();
|
||||
let mut linebreak = false;
|
||||
for line in lines {
|
||||
if line.chars().all(char::is_whitespace) {
|
||||
linebreak = true;
|
||||
} else {
|
||||
if !text.is_empty() {
|
||||
text += "\n";
|
||||
if linebreak {
|
||||
text += "\n";
|
||||
}
|
||||
}
|
||||
text += line.trim_end();
|
||||
linebreak = false;
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
let buf = buf.trim().trim_start_matches("<!doctype html>");
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
strbuilder: String::with_capacity(buf.len()),
|
||||
quote: String::new(),
|
||||
add_text: AddText::YesRemoveLineEnds,
|
||||
last_href: None,
|
||||
divs_since_quote_div: 0,
|
||||
@@ -126,22 +172,33 @@ fn dehtml_quick_xml(buf: &str) -> String {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
dehtml.strbuilder
|
||||
(dehtml.strbuilder, dehtml.quote)
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
{
|
||||
let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default();
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
|
||||
} else if !dehtml.line_prefix().is_empty() {
|
||||
let l = dehtml.append_prefix("\n");
|
||||
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
|
||||
// Replace all line ends with spaces.
|
||||
// E.g. `\r\n\r\n` is replaced with one space.
|
||||
let last_added = LINE_RE.replace_all(&last_added, " ");
|
||||
|
||||
// Add a space if `last_added` starts with a space
|
||||
// and there is no whitespace at the end of the buffer yet.
|
||||
// Trim the rest of leading whitespace from `last_added`.
|
||||
let buf = dehtml.get_buf();
|
||||
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
|
||||
*buf += " ";
|
||||
}
|
||||
|
||||
*buf += last_added.trim_start();
|
||||
} else {
|
||||
dehtml.strbuilder += &last_added;
|
||||
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,32 +209,37 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
|
||||
.to_lowercase();
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
"style" | "script" | "title" | "pre" => {
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"div" => {
|
||||
pop_tag(&mut dehtml.divs_since_quote_div);
|
||||
pop_tag(&mut dehtml.divs_since_quoted_content_div);
|
||||
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"a" => {
|
||||
if let Some(ref last_href) = dehtml.last_href.take() {
|
||||
dehtml.strbuilder += "](";
|
||||
dehtml.strbuilder += last_href;
|
||||
dehtml.strbuilder += ")";
|
||||
let buf = dehtml.get_buf();
|
||||
if buf.ends_with('[') {
|
||||
buf.truncate(buf.len() - 1);
|
||||
} else {
|
||||
*buf += "](";
|
||||
*buf += last_href;
|
||||
*buf += ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "*";
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
}
|
||||
"i" | "em" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "_";
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
|
||||
@@ -196,7 +258,9 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
|
||||
match tag.as_str() {
|
||||
"p" | "table" | "td" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
if !dehtml.strbuilder.is_empty() {
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
}
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
#[rustfmt::skip]
|
||||
@@ -204,18 +268,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
|
||||
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
|
||||
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"br" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n");
|
||||
*dehtml.get_buf() += "\n";
|
||||
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||
}
|
||||
"style" | "script" | "title" => {
|
||||
dehtml.add_text = AddText::No;
|
||||
}
|
||||
"pre" => {
|
||||
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
|
||||
*dehtml.get_buf() += "\n\n";
|
||||
dehtml.add_text = AddText::YesPreserveLineEnds;
|
||||
}
|
||||
"a" => {
|
||||
@@ -236,18 +300,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
|
||||
if !href.is_empty() {
|
||||
dehtml.last_href = Some(href);
|
||||
dehtml.strbuilder += "[";
|
||||
*dehtml.get_buf() += "[";
|
||||
}
|
||||
}
|
||||
}
|
||||
"b" | "strong" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "*";
|
||||
*dehtml.get_buf() += "*";
|
||||
}
|
||||
}
|
||||
"i" | "em" => {
|
||||
if dehtml.get_add_text() != AddText::No {
|
||||
dehtml.strbuilder += "_";
|
||||
*dehtml.get_buf() += "_";
|
||||
}
|
||||
}
|
||||
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
|
||||
@@ -308,7 +372,6 @@ pub fn dehtml_manually(buf: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
|
||||
#[test]
|
||||
fn test_dehtml() {
|
||||
@@ -322,17 +385,18 @@ mod tests {
|
||||
("<b> bar <i> foo", "* bar _ foo"),
|
||||
("& bar", "& bar"),
|
||||
// Despite missing ', this should be shown:
|
||||
("<a href='/foo.png>Hi</a> ", "Hi "),
|
||||
("<a href='/foo.png>Hi</a> ", "Hi"),
|
||||
("No link: <a href='https://get.delta.chat/'/>", "No link:"),
|
||||
(
|
||||
"<a href='https://get.delta.chat/'/>",
|
||||
"[](https://get.delta.chat/)",
|
||||
"No link: <a href='https://get.delta.chat/'></a>",
|
||||
"No link:",
|
||||
),
|
||||
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
|
||||
// Invalid html (at least DC should show the text if the html is invalid):
|
||||
("<!some invalid html code>\n<b>some text</b>", "some text"),
|
||||
];
|
||||
for (input, output) in cases {
|
||||
assert_eq!(simplify(dehtml(input).unwrap(), true).text, output);
|
||||
assert_eq!(dehtml(input).unwrap().text, output);
|
||||
}
|
||||
let none_cases = vec!["<html> </html>", ""];
|
||||
for input in none_cases {
|
||||
@@ -342,16 +406,54 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_br() {
|
||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
|
||||
let plain = dehtml(html).unwrap();
|
||||
let html = "line1<br>line2";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "line1\nline2");
|
||||
|
||||
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
|
||||
let html = "line1<br> line2";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "line1\nline2");
|
||||
|
||||
let html = "line1 <br><br> line2";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "line1\n\nline2");
|
||||
|
||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "line1\nline2\nline3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_span() {
|
||||
assert_eq!(dehtml("<span>Foo</span>bar").unwrap().text, "Foobar");
|
||||
assert_eq!(dehtml("<span>Foo</span> bar").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("<span>Foo </span>bar").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("<span>Foo</span>\nbar").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("\n<span>Foo</span> bar").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("<span>Foo</span>\n\nbar").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("Foo\n<span>bar</span>").unwrap().text, "Foo bar");
|
||||
assert_eq!(dehtml("Foo<span>\nbar</span>").unwrap().text, "Foo bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_p() {
|
||||
let html = "<p>Foo</p><p>Bar</p>";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "Foo\n\nBar");
|
||||
|
||||
let html = "<p>Foo<p>Bar";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "Foo\n\nBar");
|
||||
|
||||
let html = "<p>Foo</p><p>Bar<p>Baz";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "Foo\n\nBar\n\nBaz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_href() {
|
||||
let html = "<a href=url>text</a";
|
||||
let plain = dehtml(html).unwrap();
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
|
||||
assert_eq!(plain, "[text](url)");
|
||||
}
|
||||
@@ -359,7 +461,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dehtml_bold_text() {
|
||||
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
|
||||
let plain = dehtml(html).unwrap();
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
|
||||
assert_eq!(plain, "text *bold*<>");
|
||||
}
|
||||
@@ -369,7 +471,7 @@ mod tests {
|
||||
let html =
|
||||
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
||||
|
||||
let plain = dehtml(html).unwrap();
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
|
||||
assert_eq!(
|
||||
plain,
|
||||
@@ -393,32 +495,38 @@ mod tests {
|
||||
</html>
|
||||
"##;
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.trim(), "lots of text");
|
||||
assert_eq!(txt.text.trim(), "lots of text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_tag() {
|
||||
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.trim(), "two\nlines");
|
||||
assert_eq!(txt.text.trim(), "two\nlines");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote_div() {
|
||||
let input = include_str!("../test-data/message/gmx-quote-body.eml");
|
||||
let dehtml = dehtml(input).unwrap();
|
||||
println!("{dehtml}");
|
||||
let SimplifiedText {
|
||||
text,
|
||||
is_forwarded,
|
||||
is_cut,
|
||||
top_quote,
|
||||
footer,
|
||||
} = simplify(dehtml, false);
|
||||
} = dehtml;
|
||||
assert_eq!(text, "Test");
|
||||
assert_eq!(is_forwarded, false);
|
||||
assert_eq!(is_cut, false);
|
||||
assert_eq!(top_quote.as_deref(), Some("test"));
|
||||
assert_eq!(footer, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spaces() {
|
||||
let input = include_str!("../test-data/spaces.html");
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.text, "Welcome back to Strolling!\n\nHey there,\n\nWelcome back! Use this link to securely sign in to your Strolling account:\n\nSign in to Strolling\n\nFor your security, the link will expire in 24 hours time.\n\nSee you soon!\n\nYou can also copy\n\nhttps://strolling.rosano.ca/members/?token=XXX\n\nIf you did not make this request, you can safely ignore this email.\n\nThis message was sent from [strolling.rosano.ca](https://strolling.rosano.ca/) to [alice@example.org](mailto:alice@example.org)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ mod tests {
|
||||
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
msg.set_text("Hi Bob".to_owned());
|
||||
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
@@ -355,7 +355,6 @@ mod tests {
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(msg
|
||||
.get_text()
|
||||
.unwrap()
|
||||
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
|
||||
|
||||
receive_imf_inner(
|
||||
@@ -370,7 +369,7 @@ mod tests {
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
|
||||
assert_eq!(msg.get_text(), "100k text...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user