Compare commits

..

5 Commits

Author SHA1 Message Date
link2xt
5820c4ce95 Serialize mime_compressed 2024-04-06 16:37:51 +00:00
link2xt
12ba33d9d4 Serialize uid 2024-04-06 15:54:12 +00:00
link2xt
60a7bbc9b5 Do not serialize is_default
It is only stored for compatibility with old versions
2024-04-06 15:27:11 +00:00
link2xt
e9f434b562 Serialize backward_verified_key_id 2024-04-06 02:48:27 +00:00
link2xt
2423cb8175 feat: add support for dumping the database to stream 2024-04-06 01:45:14 +00:00
223 changed files with 12386 additions and 28322 deletions

View File

@@ -7,10 +7,3 @@ updates:
commit-message:
prefix: "chore(cargo)"
open-pull-requests-limit: 50
# Keep GitHub Actions up to date.
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.82.0
RUSTUP_TOOLCHAIN: 1.77.1
steps:
- uses: actions/checkout@v4
with:
@@ -40,18 +40,6 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
npm_constants:
name: Check if node constants are up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Rebuild constants
run: npm run build:core:constants
- name: Check that constants are not changed
run: git diff --exit-code
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
@@ -59,7 +47,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: --all-features --workspace
command: check
@@ -95,11 +83,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.82.0
rust: 1.77.1
- os: windows-latest
rust: 1.82.0
rust: 1.77.1
- os: macos-latest
rust: 1.82.0
rust: 1.77.1
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
@@ -117,20 +105,10 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Install nextest
uses: taiki-e/install-action@v2
with:
tool: nextest
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo nextest run --workspace
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --doc
run: cargo test --workspace
- name: Test cargo vendor
run: cargo vendor
@@ -211,9 +189,9 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.13
python: 3.12
- os: macos-latest
python: 3.13
python: 3.12
# PyPy tests
- os: ubuntu-latest
@@ -249,7 +227,7 @@ jobs:
- name: Run python tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
@@ -263,11 +241,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.13
python: 3.12
- os: macos-latest
python: 3.13
python: 3.12
- os: windows-latest
python: 3.13
python: 3.12
# PyPy tests
- os: ubuntu-latest
@@ -314,6 +292,6 @@ jobs:
- name: Run deltachat-rpc-client tests
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
working-directory: deltachat-rpc-client
run: tox -e py

View File

@@ -266,141 +266,3 @@ jobs:
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
permissions:
id-token: write
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
run: |
cd deltachat-rpc-server/npm-package
python --version
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
ls -lah platform_package
for platform in ./platform_package/*; do npm pack "$platform"; done
npm pack
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
if-no-files-found: error
- name: Upload npm packets to the GitHub release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.2.0
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR

View File

@@ -1,38 +1,82 @@
name: "Publish @deltachat/jsonrpc-client"
name: "jsonrpc js client build"
on:
workflow_dispatch:
release:
types: [published]
pull_request:
push:
tags:
- "*"
- "!py-*"
jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
runs-on: ubuntu-20.04
permissions:
id-token: write
contents: read
steps:
- name: Install tree
run: sudo apt install tree
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
node-version: "18"
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pull Request ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
shell: bash
working-directory: deltachat-jsonrpc/typescript
run: |
npm run build
npm pack .
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v4
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: Post links to details
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

View File

@@ -33,7 +33,7 @@ jobs:
working-directory: deltachat-jsonrpc/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver

View File

@@ -1,22 +0,0 @@
name: Test Nix flake
on:
pull_request:
push:
branches:
- main
jobs:
format:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code

View File

@@ -31,7 +31,7 @@ jobs:
mv docs js
- name: Upload
uses: horochx/deploy-via-scp@1.1.0
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}

View File

@@ -64,5 +64,5 @@ jobs:
working-directory: node
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,10 +1,9 @@
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
on:
push:
branches:
- main
- build_jsonrpc_docs_ci
jobs:
build-rs:
@@ -18,11 +17,13 @@ jobs:
run: |
cargo doc --package deltachat --no-deps --document-private-items
- name: Upload to rs.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
@@ -55,37 +56,9 @@ jobs:
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
build-ts:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: npm install
run: npm install
- name: npm run build
run: npm run build
- name: Run docs script
run: npm run docs
- name: Upload to js.jsonrpc.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"

View File

@@ -1,5 +1,5 @@
# GitHub Actions workflow
# to build `deltachat_ffi` crate documentation
# to build `deltachat_fii` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat

4
.gitignore vendored
View File

@@ -33,7 +33,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode
.vscode/launch.json
python/accounts.txt
python/all-testaccounts.txt
tmp/
@@ -50,4 +50,4 @@ result
# direnv
.envrc
.direnv
.direnv

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,6 @@ else()
set(DYNAMIC_EXT "dll")
endif()
if(DEFINED ENV{CARGO_BUILD_TARGET})
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
else()
set(ARCH_DIR "./")
endif()
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
@@ -27,7 +21,7 @@ add_custom_command(
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
@@ -41,6 +35,6 @@ add_custom_target(
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

View File

@@ -32,66 +32,6 @@ on the contributing page: <https://github.com/deltachat/deltachat-core-rust/cont
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].
### SQL
Multi-line SQL statements should be formatted using string literals,
for example
```
sql.execute(
"CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '' NOT NULL -- message text
) STRICT",
)
.await?;
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
"CREATE TABLE messages ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
text TEXT DEFAULT '' NOT NULL \
) STRICT",
)
.await?;
```
Escaping newlines
is prone to errors like this if space before backslash is missing:
```
"SELECT foo\
FROM bar"
```
Literal above results in `SELECT fooFROM bar` string.
This style also does not allow using `--` comments.
---
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
to make SQLite check column types.
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
This avoids reuse of the row IDs and can avoid dangerous bugs
like forwarding wrong message because the message was deleted
and another message took its row ID.
Declare all new columns as `NOT NULL`
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
Use `HAVING COUNT(*) > 0` clause
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
an older version. Also don't change the column type, consider adding a new column with another name
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
keyword doesn't help here.
### Commit messages
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.

3633
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.149.0"
version = "1.137.2"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -34,93 +34,95 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { workspace = true }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.1"
async-channel = { workspace = true }
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
anyhow = "1"
async-channel = "2.0.0"
async-imap = { version = "0.9.7", 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.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "7", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = { version = "4", default-features=false, features = ["std"] }
bitflags = "1.3"
bstr = { version = "1.4.0", default-features=false, features = ["std", "alloc"] }
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" }
escaper = "0.1"
fast-socks5 = "0.9"
fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
futures = "0.3"
futures-lite = "2.3.0"
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.2"
http-body-util = "0.1.2"
hickory-resolver = "0.24"
humansize = "2"
hyper = "1"
hyper-util = "0.1.10"
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
iroh-net = { version = "0.28.1", default-features = false }
kamadak-exif = "0.6.0"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mailparse = "0.15"
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
num-traits = "0.2"
once_cell = "1.18.0"
percent-encoding = "2.3"
pgp = { version = "0.14.0", default-features = false }
parking_lot = "0.12"
pgp = { version = "0.11", default-features = false }
pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.37"
quick-xml = "0.31"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rand = "0.8"
regex = "1.10"
reqwest = { version = "0.12.2", features = ["json"] }
rusqlite = { version = "0.31", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.10.0"
rustls = { version = "0.23.14", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sanitize-filename = "0.5"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
smallvec = "1.13.2"
sha2 = "0.10"
smallvec = "1"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = { workspace = true }
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.0", default-features = false }
tokio-stream = { version = "0.1.16", features = ["fs"] }
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tokio-util = "0.7.9"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.6"
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
pretty_assertions = "1.4.1"
futures-lite = "2.3.0"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
tempfile = "3"
testdir = "0.9.0"
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
[workspace]
members = [
@@ -132,7 +134,6 @@ members = [
"deltachat-repl",
"deltachat-time",
"format-flowed",
"deltachat-contact-tools",
]
[[bench]]
@@ -163,40 +164,11 @@ harness = false
name = "send_events"
harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.31"
futures-lite = "2.4.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.20.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.13.0"
thiserror = "1"
tokio = "1"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"
[features]
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl"
"async-native-tls/vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

View File

@@ -6,9 +6,6 @@
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
</a>
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
</a>
</p>
<p align="center">
@@ -30,13 +27,13 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
$ cargo run -p deltachat-repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
Optionally, install `deltachat-repl` binary with
```
$ cargo install --locked --path deltachat-repl/
$ cargo install --path deltachat-repl/
```
and run as
```
@@ -195,7 +192,6 @@ or its language bindings:
- [Desktop](https://github.com/deltachat/deltachat-desktop)
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -4,18 +4,21 @@ 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. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
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. add a link to compare previous with current version to the end of CHANGELOG.md:
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. add a link to compare previous with current version to the end of CHANGELOG.md:
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
5. 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`.
6. 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. Tag the release: `git tag -a v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
8. Push the release tag: `git push origin v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,12 +0,0 @@
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
<g
style="fill:#ffffff"
transform="scale(1.1342891,0.88160947)">
<path
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
</g>

Binary file not shown.

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;

View File

@@ -1,18 +0,0 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -1,811 +0,0 @@
//! Contact-related tools, like parsing vcards and sanitizing name and address
#![forbid(unsafe_code)]
#![warn(
unused,
clippy::correctness,
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
clippy::unused_async,
clippy::explicit_iter_loop,
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string,
clippy::bool_to_int_with_if,
clippy::manual_range_contains
)]
use std::fmt;
use std::ops::Deref;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use chrono::{DateTime, NaiveDateTime};
use once_cell::sync::Lazy;
use regex::Regex;
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:{addr}\n\
FN:{display_name}\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\n");
}
res += "END:VCARD\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
let remainder = remove_prefix(s, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// Note: This doesn't handle the case where there are quotes around a colon,
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
// This could be improved in the future, but for now, the parsing is good enough.
let (params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
Some(value)
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for mut line in lines.by_ref() {
if let Some(remainder) = remove_prefix(line, "item1.") {
// Remove the group name, if the group is called "item1".
// If necessary, we can improve this to also remove groups that are called something different that "item1".
//
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
line = remainder;
}
if let Some(email) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some(name) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
{
key.get_or_insert(k);
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
{
photo.get_or_insert(p);
} else if let Some(rev) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
break;
}
}
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
}
contacts
}
/// Valid contact address.
#[derive(Debug, Clone)]
pub struct ContactAddress(String);
impl Deref for ContactAddress {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for ContactAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContactAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ContactAddress {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr.to_string()))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Takes a name and an address and sanitizes them:
/// - Extracts a name from the addr if the addr is in form "Alice <alice@example.org>"
/// - Removes special characters from the name, see [`sanitize_name()`]
/// - Removes the name if it is equal to the address by setting it to ""
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
captures.get(1).map_or("", |m| m.as_str())
} else {
name
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(name, addr.to_string())
};
let mut name = sanitize_name(name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();
}
(name, addr)
}
/// Sanitizes a name.
///
/// - Removes newlines and trims the string
/// - Removes quotes (come from some bad MUA implementations)
/// - Removes potentially-malicious bidi characters
pub fn sanitize_name(name: &str) -> String {
let name = sanitize_single_line(name);
match name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => name
.get(1..name.len() - 1)
.map_or("".to_string(), |s| s.trim().to_string()),
_ => name.to_string(),
}
}
/// Sanitizes user input
///
/// - Removes newlines and trims the string
/// - Removes potentially-malicious bidi characters
pub fn sanitize_single_line(input: &str) -> String {
sanitize_bidi_characters(input.replace(['\n', '\r'], " ").trim())
}
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
const ISOLATE_CHARACTERS: [char; 3] = ['\u{2066}', '\u{2067}', '\u{2068}'];
const POP_ISOLATE_CHARACTER: char = '\u{2069}';
/// Some control unicode characters can influence whether adjacent text is shown from
/// left to right or from right to left.
///
/// Since user input is not supposed to influence how adjacent text looks,
/// this function removes some of these characters.
///
/// Also see https://github.com/deltachat/deltachat-core-rust/issues/3479.
pub fn sanitize_bidi_characters(input_str: &str) -> String {
// RTLO_CHARACTERS are apparently rarely used in practice.
// They can impact all following text, so, better remove them all:
let input_str = input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "");
// If the ISOLATE characters are not ended with a POP DIRECTIONAL ISOLATE character,
// we regard the input as potentially malicious and simply remove all ISOLATE characters.
// See https://en.wikipedia.org/wiki/Bidirectional_text#Unicode_bidi_support
// and https://www.w3.org/International/questions/qa-bidi-unicode-controls.en
// for an explanation about ISOLATE characters.
fn isolate_characters_are_valid(input_str: &str) -> bool {
let mut isolate_character_nesting: i32 = 0;
for char in input_str.chars() {
if ISOLATE_CHARACTERS.contains(&char) {
isolate_character_nesting += 1;
} else if char == POP_ISOLATE_CHARACTER {
isolate_character_nesting -= 1;
}
// According to Wikipedia, 125 levels are allowed:
// https://en.wikipedia.org/wiki/Unicode_control_characters
// (although, in practice, we could also significantly lower this number)
if isolate_character_nesting < 0 || isolate_character_nesting > 125 {
return false;
}
}
isolate_character_nesting == 0
}
if isolate_characters_are_valid(&input_str) {
input_str
} else {
input_str.replace(
|char| ISOLATE_CHARACTERS.contains(&char) || POP_ISOLATE_CHARACTER == char,
"",
)
}
}
/// Returns false if addr is an invalid address, otherwise true.
pub fn may_be_valid_addr(addr: &str) -> bool {
let res = EmailAddress::new(addr);
res.is_ok()
}
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(&norm).to_string()
} else {
norm
}
}
/// Compares two email addresses, normalizing them beforehand.
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
norm1 == norm2
}
///
/// Represents an email address, right now just the `name@domain` portion.
///
/// # Example
///
/// ```
/// use deltachat_contact_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
/// assert_eq!(&email.local, "someone");
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
/// Local part of the email address.
pub local: String,
/// Email address domain.
pub domain: String,
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
}
}
impl EmailAddress {
/// Performs a dead-simple parse of an email address.
pub fn new(input: &str) -> Result<EmailAddress> {
if input.is_empty() {
bail!("empty string is not valid");
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
if input
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
bail!("empty string is not valid for local part in {:?}", input);
}
if domain.is_empty() {
bail!("missing domain after '@' in {:?}", input);
}
if domain.ends_with('.') {
bail!("Domain {domain:?} should not contain the dot in the end");
}
Ok(EmailAddress {
local: (*local).to_string(),
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
}
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:alice@example.org\n\
FN:Alice Wonderland\n\
KEY:data:application/pgp-keys;base64,[base64-data]\n\
PHOTO:data:image/jpeg;base64,image in Base64\n\
REV:20240418T184242Z\n\
END:VCARD\n",
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:bob@example.com\n\
FN:bob@example.com\n\
REV:19700101T000000Z\n\
END:VCARD\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
let contact_address = ContactAddress::new(alice_addr)?;
assert_eq!(contact_address.as_ref(), alice_addr);
let invalid_addr = "<> foobar";
assert!(ContactAddress::new(invalid_addr).is_err());
Ok(())
}
#[test]
fn test_emailaddress_parse() {
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!(
EmailAddress::new("user@domain.tld").unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(
EmailAddress::new("user@localhost").unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert!(EmailAddress::new("tt.dd@uu").is_ok());
assert!(EmailAddress::new("u@d").is_ok());
assert!(EmailAddress::new("u@d.").is_err());
assert!(EmailAddress::new("u@d.t").is_ok());
assert_eq!(
EmailAddress::new("u@d.tt").unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert!(EmailAddress::new("u@tt").is_ok());
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing.
// This one is Android-like.
let vcard0 = "BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
";
// This one is DOS-like.
let vcard1 = vcard0.replace('\n', "\r\n");
for vcard in [vcard0, vcard1.as_str()] {
let contacts = parse_vcard(vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
#[test]
fn test_protonmail_vcard() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN;PREF=1:Alice Wonderland
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
ITEM1.EMAIL;PREF=1:alice@example.org
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
ITEM1.X-PM-ENCRYPT:true
ITEM1.X-PM-SIGN:true
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice Wonderland");
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image, None);
}
#[test]
fn test_sanitize_name() {
assert_eq!(&sanitize_name(" hello world "), "hello world");
assert_eq!(&sanitize_name("<"), "<");
assert_eq!(&sanitize_name(">"), ">");
assert_eq!(&sanitize_name("'"), "'");
assert_eq!(&sanitize_name("\""), "\"");
}
#[test]
fn test_sanitize_single_line() {
assert_eq!(sanitize_single_line("Hi\naiae "), "Hi aiae");
assert_eq!(sanitize_single_line("\r\nahte\n\r"), "ahte");
}
#[test]
fn test_sanitize_bidi_characters() {
// Legit inputs:
assert_eq!(
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat\u{2069}"),
"Tes\u{2067}ting Delta Chat\u{2069}"
);
assert_eq!(
&sanitize_bidi_characters("Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"),
"Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"
);
assert_eq!(
&sanitize_bidi_characters("Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"),
"Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"
);
// Potentially-malicious inputs:
assert_eq!(
&sanitize_bidi_characters("Tes\u{202C}ting Delta Chat"),
"Testing Delta Chat"
);
assert_eq!(
&sanitize_bidi_characters("Testing Delta Chat\u{2069}"),
"Testing Delta Chat"
);
assert_eq!(
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat"),
"Testing Delta Chat"
);
assert_eq!(
&sanitize_bidi_characters("Tes\u{2069}ting Delta Chat\u{2067}"),
"Testing Delta Chat"
);
assert_eq!(
&sanitize_bidi_characters("Tes\u{2068}ting Delta Chat"),
"Testing Delta Chat"
);
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.149.0"
version = "1.137.2"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -14,21 +14,21 @@ name = "deltachat"
crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { workspace = true, default-features = false }
deltachat-jsonrpc = { workspace = true, optional = true }
libc = { workspace = true }
human-panic = { version = "2", default-features = false }
num-traits = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
anyhow = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
once_cell = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose"] }
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = { version = "1", default-features = false }
num-traits = "0.2"
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"
once_cell = "1.18.0"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features]
default = ["vendored"]
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
vendored = ["deltachat/vendored"]
jsonrpc = ["dep:deltachat-jsonrpc"]

View File

@@ -17,6 +17,7 @@ typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
@@ -362,12 +363,8 @@ uint32_t dc_get_id (dc_context_t* context);
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per context.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* may or may not be available to event emitter.
* Having more than one event emitter running at the same time on the same context
* will result in events being randomly delivered to one of the emitters.
*/
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
@@ -403,10 +400,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `socks5_enabled` = SOCKS5 enabled
* - `socks5_host` = SOCKS5 proxy server host
* - `socks5_port` = SOCKS5 proxy server port
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
@@ -417,10 +417,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* 1=send and request read receipts (default)
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
@@ -479,9 +478,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages,
* accepts contact requests automatically (calling dc_accept_chat() is not needed),
* does not cut large incoming text messages,
* handles existing messages the same way as new ones if `fetch_existing_msgs=1`.
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
* and does not cut large incoming text messages.
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
@@ -492,8 +490,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* For most bots calling `dc_markseen_msgs()` is the
* recommended way to update this value
* even for self-sent messages.
* - `fetch_existing_msgs` = 0=do not fetch existing messages on configure (default),
* 1=fetch most recent existing messages on configure.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
* 0=use IMAP IDLE if the server supports it.
@@ -506,11 +504,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
@@ -521,22 +514,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
* In contrast to `dc_set_chat_mute_duration()`,
* fresh message and badge counters are not changed by this setting,
* but should be tuned down where appropriate.
* - `private_tag` = Optional tag as "Work", "Family".
* Meant to help profile owner to differ between profiles with similar names.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default).
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -871,10 +853,13 @@ void dc_maybe_network (dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data Ignored, actual public key is extracted from secret_data.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *secret_data);
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
@@ -1132,6 +1117,36 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*
* @deprecated 2023-11-27, use jsonrpc method `send_reaction` instead
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -1194,65 +1209,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
/**
* Set Webxdc file as integration.
* see dc_init_webxdc_integration() for more details about Webxdc integrations.
*
* @warning This is an experimental API which may change in the future
*
* @memberof dc_context_t
* @param context The context object.
* @param file The .xdc file to use as Webxdc integration.
*/
void dc_set_webxdc_integration (dc_context_t* context, const char* file);
/**
* Init a Webxdc integration.
*
* A Webxdc integration is
* a Webxdc showing a map, getting locations via setUpdateListener(), setting POIs via sendUpdate();
* core takes eg. care of feeding locations to the Webxdc or sending the data out.
*
* @warning This is an experimental API, esp. support of integration types (eg. image editor, tools) is left out for simplicity
*
* Currently, Webxdc integrations are .xdc files shipped together with the main app.
* Before dc_init_webxdc_integration() can be called,
* UI has to call dc_set_webxdc_integration() to define a .xdc file to be used as integration.
*
* dc_init_webxdc_integration() returns a Webxdc message ID that
* UI can open and use mostly as usual.
*
* Concrete behaviour and status updates depend on the integration, driven by UI needs.
*
* There is no need to de-initialize the integration,
* however, unless documented otherwise,
* the integration is valid only as long as not re-initialized
* In other words, UI must not have a Webxdc with the same integration open twice.
*
* Example:
*
* ~~~
* // Define a .xdc file to be used as maps integration
* dc_set_webxdc_integration(context, path_to_maps_xdc);
*
* // Integrate the map to a chat, the map will show locations for this chat then:
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, any_chat_id);
*
* // Or use the Webxdc as a global map, showing locations of all chats:
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, 0);
* ~~~
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to get the integration for.
* @return ID of the message that refers to the Webxdc instance.
* UI can open a Webxdc as usual with this instance.
*/
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
/**
* Save a draft for a chat in the database.
*
@@ -1554,6 +1510,30 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t chat_id, int msg_type, int msg_type2, int msg_type3);
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
* @param dir 1=get the next message, -1=get the previous one.
* @param msg_type The message type to search for.
* If 0, the message type from curr_msg_id is used.
* @param msg_type2 Alternative message type to search for. 0 to skip.
* @param msg_type3 Alternative message type to search for. 0 to skip.
* @return Returns the message ID that should be played next.
* The returned message is in the same chat as the given one
* and has one of the given types.
* Typically, this result is passed again to dc_get_next_media()
* later on the next swipe.
* If there is not next/previous message, the function returns 0.
*/
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -2482,9 +2462,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_BACKUP 251
#define DC_QR_BACKUP2 252
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2530,7 +2508,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_BACKUP:
* - DC_QR_BACKUP2:
* ask the user if they want to set up a new device.
* If so, pass the qr-code to dc_receive_backup().
*
@@ -2538,12 +2515,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
* On success, dc_get_config(context, "proxy_url")
* will contain the new proxy in normalized form as the first element.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
@@ -2605,7 +2576,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://securejoin.delta.chat/
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return The text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
@@ -2618,7 +2589,6 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
* See dc_get_securejoin_qr() for details about the contained QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
* @memberof dc_context_t
* @param context The context object.
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
@@ -2642,7 +2612,8 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
*
* See https://securejoin.delta.chat/ for details about both protocols.
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
* @param context The context object.
@@ -2799,22 +2770,6 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
// misc
/**
* Create a QR code from any input data.
*
* The QR code is returned as a square SVG image.
*
* @memberof dc_context_t
* @param payload The content for the QR code.
* @return SVG image with the QR code.
* On errors, an empty string is returned.
* The returned string must be released using dc_str_unref() after usage.
*/
char* dc_create_qr_svg (const char* payload);
/**
* Get last error string.
*
@@ -2903,7 +2858,6 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
* This works like dc_backup_provider_qr() but returns the text of a rendered
* SVG image containing the QR code.
*
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -2943,7 +2897,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
* Gets a backup offered by a dc_backup_provider_t object on another device.
*
* This function is called on a device that scanned the QR code offered by
* dc_backup_provider_get_qr(). Typically this is a
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
* different device than that which provides the backup.
*
* This call will block while the backup is being transferred and only
@@ -4120,19 +4074,6 @@ char* dc_msg_get_subject (const dc_msg_t* msg);
char* dc_msg_get_file (const dc_msg_t* msg);
/**
* Save file copy at the user-provided path.
*
* Fails if file already exists at the provided path.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param path Destination file path with filename and extension.
* @return 0 on failure, 1 on success.
*/
int dc_msg_save_file (const dc_msg_t* msg, const char* path);
/**
* Get an original attachment filename, with extension but without the path. To get the full path,
* use dc_msg_get_file().
@@ -4197,6 +4138,7 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* this is useful for development and maybe for internal integrations at some point.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4405,9 +4347,9 @@ int dc_msg_has_deviating_timestamp(const dc_msg_t* msg);
/**
* Check if a message has a POI location bound to it.
* These locations are also returned by dc_get_locations()
* The UI may decide to display a special icon beside such messages.
* Check if a message has a location bound to it.
* These messages are also returned by dc_get_locations()
* and the UI may decide to display a special icon beside such messages,
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -5378,6 +5320,52 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @deprecated 2023-11-27
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
/**
* @defgroup DC_MSG DC_MSG
*
@@ -5494,11 +5482,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_WEBXDC 80
/**
* Message containing shared contacts represented as a vCard (virtual contact file)
* with email addresses and possibly other fields.
*/
#define DC_MSG_VCARD 90
/**
* @}
@@ -5711,14 +5694,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CERTCK_STRICT 1
/**
* Accept certificates that are expired, self-signed
* or not valid for the server hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID 2
/**
* For API compatibility only: Treat this as DC_CERTCK_ACCEPT_INVALID on reading.
* Must not be written.
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
*/
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
@@ -6064,21 +6041,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* A reaction to one's own sent message received.
* Typically, the UI will show a notification for that.
*
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
*
* @param data1 (int) contact_id ID of the contact sending this reaction.
* @param data2 (int) msg_id + (char*) reaction.
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
* and the reaction as dc_event_get_data2_str().
* string must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_INCOMING_REACTION 2002
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
@@ -6296,7 +6258,7 @@ void dc_event_unref(dc_event_t* event);
/**
* Webxdc status update received.
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates() with
* `serial` set to the last known update
* (in case of special bots, `status_update_serial` from `data2`
@@ -6319,27 +6281,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
* Data received over an ephemeral peer channel.
*
* @param data1 (int) msg_id
* @param data2 (int) + (char*) binary data.
* length is returned as integer with dc_event_get_data2_int()
* and binary data is returned as dc_event_get_data2_str().
* Binary data must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
/**
* Advertisement for ephemeral peer channel communication received.
* This can be used by bots to initiate peer-to-peer communication from their side.
* @param data1 (int) msg_id
* @param data2 0
*/
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
/**
* Tells that the Background fetch was completed (or timed out).
*
@@ -6349,32 +6290,7 @@ void dc_event_unref(dc_event_t* event);
* This event is only emitted by the account manager
*/
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
/**
* Inform that set of chats or the order of the chats in the chatlist has changed.
*
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
*/
#define DC_EVENT_CHATLIST_CHANGED 2300
/**
* Inform that all or a single chat list item changed and needs to be rerendered
* If `chat_id` is set to 0, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
*
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
*/
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that some events have been skipped due to event channel overflow.
*
* @param data1 (int) number of events that have been skipped
*/
#define DC_EVENT_CHANNEL_OVERFLOW 2400
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
/**
* @}
@@ -6683,16 +6599,12 @@ void dc_event_unref(dc_event_t* event);
/// "Message opened"
///
/// Used in subjects of outgoing read receipts.
///
/// @deprecated Deprecated 2024-07-26
#define DC_STR_READRCPT 31
/// "The message '%1$s' you sent was displayed on the screen of the recipient."
///
/// Used as message text of outgoing read receipts.
/// - %1$s will be replaced by the subject of the displayed message
///
/// @deprecated Deprecated 2024-06-23
#define DC_STR_READRCPT_MAILBODY 32
/// @deprecated Deprecated, this string is no longer needed.
@@ -7401,19 +7313,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/// "Establishing guaranteed end-to-end encryption, please wait…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200
/**
* @}
*/

View File

@@ -1,9 +1,10 @@
#![recursion_limit = "256"]
#![warn(unused, clippy::all)]
#![allow(
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
non_upper_case_globals,
non_camel_case_types,
clippy::missing_safety_doc,
clippy::expect_fun_call
)]
@@ -30,7 +31,8 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
@@ -66,6 +68,8 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -541,7 +545,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
@@ -563,15 +566,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ConfigSynced { .. } => 2111,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::WebxdcRealtimeData { .. } => 2150,
EventType::WebxdcRealtimeAdvertisementReceived { .. } => 2151,
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::EventChannelOverflow { .. } => 2400,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -601,8 +596,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -625,17 +618,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
EventType::EventChannelOverflow { n } => *n as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -672,15 +656,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => 0,
| EventType::ConfigSynced { .. } => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -693,10 +672,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -745,11 +720,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
| EventType::IncomingMsgBunch { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -765,19 +736,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::WebxdcRealtimeData { data, .. } => {
let ptr = libc::malloc(data.len());
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
ptr as *mut libc::c_char
}
EventType::IncomingReaction { reaction, .. } => reaction
.as_str()
.to_c_string()
.unwrap_or_default()
.into_raw(),
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
@@ -859,6 +817,8 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
_public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -866,8 +826,9 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
let addr = to_string_lossy(addr);
let secret_data = to_string_lossy(secret_data);
block_on(preconfigure_keypair(ctx, &secret_data))
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
.context("Failed to save keypair")
.log_err(ctx)
.is_ok() as libc::c_int
@@ -1054,6 +1015,49 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.context("failed dc_get_msg_reactions() call")
.log_err(ctx)
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -1097,43 +1101,6 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_webxdc_integration(
context: *mut dc_context_t,
file: *const libc::c_char,
) {
if context.is_null() || file.is_null() {
eprintln!("ignoring careless call to dc_set_webxdc_integration()");
return;
}
let ctx = &*context;
block_on(ctx.set_webxdc_integration(&to_string_lossy(file)))
.log_err(ctx)
.unwrap_or_default();
}
#[no_mangle]
pub unsafe extern "C" fn dc_init_webxdc_integration(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_init_webxdc_integration()");
return 0;
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
} else {
Some(ChatId::new(chat_id))
};
block_on(ctx.init_webxdc_integration(chat_id))
.log_err(ctx)
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1467,6 +1434,48 @@ pub unsafe extern "C" fn dc_get_chat_media(
})
}
#[no_mangle]
#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
dir: libc::c_int,
msg_type: libc::c_int,
or_msg_type2: libc::c_int,
or_msg_type3: libc::c_int,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_next_media()");
return 0;
}
let direction = if dir < 0 {
chat::Direction::Backward
} else {
chat::Direction::Forward
};
let ctx = &*context;
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
let or_msg_type3 =
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
block_on(async move {
chat::get_next_media(
ctx,
MsgId::new(msg_id),
direction,
msg_type,
or_msg_type2,
or_msg_type3,
)
.await
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0)
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -2037,7 +2046,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
);
message::Message::default()
} else {
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
return ptr::null_mut();
}
}
@@ -2594,18 +2603,6 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
if payload.is_null() {
eprintln!("ignoring careless call to dc_create_qr_svg()");
return "".strdup();
}
create_qr_svg(&to_string_lossy(payload))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -3372,34 +3369,6 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
.unwrap_or_else(|| "".strdup())
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_save_file(
msg: *mut dc_msg_t,
path: *const libc::c_char,
) -> libc::c_int {
if msg.is_null() || path.is_null() {
eprintln!("ignoring careless call to dc_msg_save_file()");
return 0;
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
let path = to_string_lossy(path);
let r = block_on(
ffi_msg
.message
.save_file(ctx, &std::path::PathBuf::from(path)),
);
match r {
Ok(()) => 1,
Err(_) => {
r.context("Failed to save file from message")
.log_err(ctx)
.unwrap_or_default();
0
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -4275,6 +4244,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
@@ -4355,7 +4363,7 @@ pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provid
let ctx = &*ffi_provider.context;
let provider = &mut ffi_provider.provider;
block_on(provider)
.context("Failed to await backup provider")
.context("Failed to await BackupProvider")
.log_err(ctx)
.set_last_error(ctx)
.ok();
@@ -4409,7 +4417,7 @@ trait ResultExt<T, E> {
/// Like `log_err()`, but:
/// - returns the default value instead of an Err value.
/// - emits an error instead of a warning for an [Err] result. This means
/// that the error will be shown to the user in a small pop-up.
/// that the error will be shown to the user in a small pop-up.
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
}
@@ -4475,6 +4483,19 @@ where
}
}
trait ResultNullableExt<T> {
fn into_raw(self) -> *mut T;
}
impl<T, E> ResultNullableExt<T> for Result<T, E> {
fn into_raw(self) -> *mut T {
match self {
Ok(t) => Box::into_raw(Box::new(t)),
Err(_) => ptr::null_mut(),
}
}
}
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
let msg_ids: Vec<MsgId> = ids
@@ -4528,16 +4549,19 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.context("Can't get config")
.log_err(ctx)
});
match proxy_enabled {
Ok(proxy_enabled) => {
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
proxy_enabled,
socks5_enabled,
))
.log_err(ctx)
.unwrap_or_default()
@@ -4868,7 +4892,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.maybe_network_lost().await });
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
#[no_mangle]
@@ -4882,12 +4906,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
}
let accounts = &*accounts;
let background_fetch_future = {
let lock = block_on(accounts.read());
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
block_on(background_fetch_future);
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
1
}
@@ -4905,7 +4929,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
let token = to_string_lossy(token);
block_on(async move {
let accounts = accounts.read().await;
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."
@@ -4951,9 +4975,7 @@ mod jsonrpc {
}
let account_manager = &*account_manager;
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.inner.clone(),
));
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);

View File

@@ -34,34 +34,33 @@ pub enum Meaning {
}
impl Lot {
pub fn get_text1(&self) -> Option<Cow<str>> {
pub fn get_text1(&self) -> Option<&str> {
match self {
Self::Summary(summary) => match &summary.prefix {
None => None,
Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
Some(SummaryPrefix::Draft(text)) => Some(text),
Some(SummaryPrefix::Username(username)) => Some(username),
Some(SummaryPrefix::Me(text)) => Some(text),
},
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
Qr::Backup2 { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
Qr::Url { url } => Some(Cow::Borrowed(url)),
Qr::Text { text } => Some(Cow::Borrowed(text)),
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::Backup { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None,
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
Qr::Login { address, .. } => Some(address),
},
Self::Error(err) => Some(Cow::Borrowed(err)),
Self::Error(err) => Some(err),
}
}
@@ -102,9 +101,8 @@ impl Lot {
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::Backup { .. } => LotState::QrBackup,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Proxy { .. } => LotState::QrProxy,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
Qr::Text { .. } => LotState::QrText,
@@ -128,9 +126,8 @@ impl Lot {
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::Backup { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
@@ -180,14 +177,9 @@ pub enum LotState {
QrBackup = 251,
QrBackup2 = 252,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// text1=address, text2=protocol
QrProxy = 271,
/// id=contact
QrAddr = 320,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.149.0"
version = "1.137.2"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -13,30 +13,29 @@ path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true }
deltachat-contact-tools = { workspace = true }
num-traits = { workspace = true }
schemars = "0.8.21"
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
log = { workspace = true }
async-channel = { workspace = true }
futures = { workspace = true }
serde_json = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
tokio = { workspace = true }
sanitize-filename = { workspace = true }
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.10.1"
log = "0.4"
async-channel = { version = "2.0.0" }
futures = { version = "0.3.30" }
serde_json = "1"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.37.0" }
sanitize-filename = "0.5"
walkdir = "2.5.0"
base64 = { workspace = true }
base64 = "0.21"
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.5", optional = true }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -1,8 +1,5 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::str;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
@@ -18,14 +15,12 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
@@ -33,8 +28,6 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -46,7 +39,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::contact::{ContactObject, VcardContact};
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
@@ -69,14 +62,14 @@ use crate::api::types::qr::QrObject;
struct AccountState {
/// The Qr code for current [`CommandApi::provide_backup`] call.
///
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
/// `Some`, otherwise `None`.
backup_provider_qr: watch::Sender<Option<Qr>>,
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
/// `Pending` or `Ready`, otherwise `NoProvider`.
backup_provider_qr: watch::Sender<ProviderQr>,
}
impl Default for AccountState {
fn default() -> Self {
let tx = watch::Sender::new(None);
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
Self {
backup_provider_qr: tx,
}
@@ -87,30 +80,21 @@ impl Default for AccountState {
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
/// Receiver side of the event channel.
///
/// Events from it can be received by calling `get_next_event` method.
event_emitter: Arc<EventEmitter>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
let event_emitter = Arc::new(accounts.get_event_emitter());
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
#[allow(dead_code)]
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi {
accounts,
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
@@ -139,13 +123,21 @@ impl CommandApi {
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
.await;
loop {
if let Some(qr) = receiver.borrow_and_update().clone() {
return Ok(qr);
}
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
let val: ProviderQr = receiver.borrow_and_update().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => loop {
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
let val: ProviderQr = receiver.borrow().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => continue,
ProviderQr::Ready(qr) => break Ok(qr),
};
},
ProviderQr::Ready(qr) => Ok(qr),
}
}
}
@@ -173,7 +165,8 @@ impl CommandApi {
/// Get the next event.
async fn get_next_event(&self) -> Result<Event> {
self.event_emitter
let event_emitter = self.accounts.read().await.get_event_emitter();
event_emitter
.recv()
.await
.map(|event| event.into())
@@ -188,16 +181,6 @@ impl CommandApi {
self.accounts.write().await.add_account().await
}
/// Imports/migrated an existing account from a database path into this account manager.
/// Returns the ID of new account.
async fn migrate_account(&self, path_to_db: String) -> Result<u32> {
self.accounts
.write()
.await
.migrate_account(std::path::PathBuf::from(path_to_db))
.await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts
.write()
@@ -254,12 +237,11 @@ impl CommandApi {
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
let future = {
let lock = self.accounts.read().await;
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
};
// At this point account manager is not locked anymore.
future.await;
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
@@ -322,12 +304,12 @@ impl CommandApi {
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -343,11 +325,6 @@ impl CommandApi {
ctx.get_info().await
}
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
@@ -708,22 +685,7 @@ impl CommandApi {
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
///
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
/// If `chat_id` is unset, setup contact QR code is returned.
async fn get_chat_securejoin_qr_code(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
Ok(qr)
}
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
@@ -735,7 +697,8 @@ impl CommandApi {
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.delta.chat/ for details about both protocols.
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// return format: `[code, svg]`
async fn get_chat_securejoin_qr_code_svg(
@@ -745,9 +708,10 @@ impl CommandApi {
) -> Result<(String, String)> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
let svg = get_securejoin_qr_svg(&ctx, chat).await?;
Ok((qr, svg))
Ok((
securejoin::get_securejoin_qr(&ctx, chat).await?,
get_securejoin_qr_svg(&ctx, chat).await?,
))
}
/// Continue a Setup-Contact or Verified-Group-Invite protocol
@@ -762,7 +726,8 @@ impl CommandApi {
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://securejoin.delta.chat/ for details about both protocols.
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given
/// to `check_qr()`.
@@ -1135,11 +1100,9 @@ impl CommandApi {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1163,10 +1126,7 @@ impl CommandApi {
messages.insert(
message_id,
match message_result {
Ok(Some(message)) => MessageLoadResult::Message(message),
Ok(None) => MessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
@@ -1424,15 +1384,6 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
async fn change_contact_name(
&self,
account_id: u32,
@@ -1474,51 +1425,6 @@ impl CommandApi {
Ok(contact_id.map(|id| id.to_u32()))
}
/// Parses a vCard file located at the given path. Returns contacts in their original order.
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
let vcard = fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat_contact_tools::parse_vcard(vcard)
.into_iter()
.map(|c| c.into())
.collect())
}
/// Imports contacts from a vCard file located at the given path.
///
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let vcard = tokio::fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat::contact::import_vcard(&ctx, vcard)
.await?
.into_iter()
.map(|c| c.to_u32())
.collect())
}
/// Returns a vCard containing contacts with the given ids.
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
deltachat::contact::make_vcard(&ctx, &contacts).await
}
/// Sets vCard containing the given contacts to the message draft.
async fn set_draft_vcard(
&self,
account_id: u32,
msg_id: u32,
contacts: Vec<u32>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
msg.make_vcard(&ctx, &contacts).await?;
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -1567,6 +1473,55 @@ impl CommandApi {
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
/// Search next/previous message based on a given message and a list of types.
/// Typically used to implement the "next" and "previous" buttons
/// in a gallery or in a media player.
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
msg_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<(Option<u32>, Option<u32>)> {
let ctx = self.get_context(account_id).await?;
let msg_type: Viewtype = message_type.into();
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
let prev = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Backward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
let next = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Forward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
Ok((prev, next))
}
// ---------------------------------------------
// backup
// ---------------------------------------------
@@ -1614,21 +1569,20 @@ impl CommandApi {
/// Returns once a remote device has retrieved the backup, or is cancelled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(ProviderQr::Pending);
})
.await;
let provider = imex::BackupProvider::prepare(&ctx).await?;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(Some(provider.qr()));
state
.backup_provider_qr
.send_replace(ProviderQr::Ready(provider.qr()));
})
.await;
let res = provider.await;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(None);
})
.await;
res
provider.await
}
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
@@ -1636,17 +1590,11 @@ impl CommandApi {
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 60 seconds to avoid deadlocks.
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = tokio::time::timeout(
Duration::from_secs(60),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
let qr = self.inner_get_backup_qr(account_id).await?;
qr::format_backup(&qr)
}
@@ -1655,20 +1603,14 @@ impl CommandApi {
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 60 seconds to avoid deadlocks.
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(60),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
let qr = self.inner_get_backup_qr(account_id).await?;
generate_backup_qr(&ctx, &qr).await
}
@@ -1678,9 +1620,6 @@ impl CommandApi {
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
///
/// Do not forget to call start_io on the account after a successful import,
/// otherwise it will not connect to the email server.
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_text).await?;
@@ -1774,37 +1713,6 @@ impl CommandApi {
.await
}
async fn send_webxdc_realtime_data(
&self,
account_id: u32,
instance_msg_id: u32,
data: Vec<u8>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
send_webxdc_realtime_data(&ctx, MsgId::new(instance_msg_id), data).await
}
async fn send_webxdc_realtime_advertisement(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
if let Some(fut) = fut {
tokio::spawn(async move {
fut.await.ok();
info!(ctx, "send_webxdc_realtime_advertisement done")
});
}
Ok(())
}
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
}
async fn get_webxdc_status_updates(
&self,
account_id: u32,
@@ -1846,29 +1754,6 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Sets Webxdc file as integration.
/// `file` is the .xdc to use as Webxdc integration.
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.set_webxdc_integration(&file_path).await
}
/// Returns Webxdc instance used for optional integrations.
/// UI can open the Webxdc as usual.
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
/// `integrate_for` is the chat to get the integration for.
async fn init_webxdc_integration(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.init_webxdc_integration(chat_id.map(ChatId::new))
.await?
.map(|msg_id| msg_id.to_u32()))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
@@ -1961,13 +1846,9 @@ impl CommandApi {
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = data
.create_message(&ctx)
.await
.context("Failed to create message")?;
let mut message = data.create_message(&ctx).await?;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await
.context("Failed to send created message")?
.await?
.to_u32();
Ok(msg_id)
}
@@ -1981,15 +1862,6 @@ impl CommandApi {
Ok(can_send)
}
/// Saves a file copy at the user-provided path.
///
/// Fails if file already exists at the provided path.
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
message.save_file(&ctx, Path::new(&path)).await
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -2004,7 +1876,9 @@ impl CommandApi {
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(MessageObject::from_msg_id(&ctx, draft.get_id()).await?)
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
@@ -2060,21 +1934,19 @@ impl CommandApi {
);
let destination_path = account_folder.join("stickers").join(collection);
fs::create_dir_all(&destination_path).await?;
let file = message.get_filename().context("no file?")?;
message
.save_file(
&ctx,
&destination_path.join(format!(
"{}.{}",
msg_id,
Path::new(&file)
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
let file = message.get_file(&ctx).context("no file")?;
fs::copy(
&file,
destination_path.join(format!(
"{}.{}",
msg_id,
file.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
Ok(())
}
@@ -2130,7 +2002,8 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new_text(text);
let mut msg = Message::new(Viewtype::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())
@@ -2173,9 +2046,7 @@ impl CommandApi {
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id)
.await?
.context("Just sent message does not exist")?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}
@@ -2270,3 +2141,15 @@ async fn get_config(
.await
}
}
/// Whether a QR code for a BackupProvider is currently available.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum ProviderQr {
/// There is no provider, asking for a QR is an error.
NoProvider,
/// There is a provider, the QR code is pending.
Pending,
/// There is a provider and QR code.
Ready(Qr),
}

View File

@@ -17,9 +17,6 @@ pub enum Account {
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
color: String,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.
private_tag: Option<String>,
},
#[serde(rename_all = "camelCase")]
Unconfigured { id: u32 },
@@ -34,14 +31,12 @@ impl Account {
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
let private_tag = ctx.get_config(Config::PrivateTag).await?;
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
private_tag,
})
} else {
Ok(Account::Unconfigured { id })

View File

@@ -32,7 +32,6 @@ pub struct FullChat {
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: u32,
is_unpromoted: bool,
@@ -105,7 +104,6 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
@@ -155,7 +153,6 @@ pub struct BasicChat {
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
@@ -183,7 +180,6 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),

View File

@@ -88,17 +88,11 @@ pub(crate) async fn get_chat_list_item_by_id(
let (last_updated, message_type) = match last_msgid {
Some(id) => {
if let Some(last_message) =
deltachat::message::Message::load_from_db_optional(ctx, id).await?
{
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
} else {
// Message may be deleted by the time we try to load it.
(None, None)
}
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
(
Some(last_message.get_timestamp() * 1000),
Some(last_message.get_viewtype().into()),
)
}
None => (None, None),
};

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -19,7 +18,6 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
///
@@ -80,7 +78,6 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
verifier_id,
@@ -90,35 +87,3 @@ impl ContactObject {
})
}
}
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VcardContact {
/// Email address.
addr: String,
/// The contact's name, or the email address if no name was given.
display_name: String,
/// Public PGP key in Base64.
key: Option<String>,
/// Profile image in Base64.
profile_image: Option<String>,
/// Contact color as hex string.
color: String,
/// Last update timestamp.
timestamp: Option<i64>,
}
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,
key: vc.key,
profile_image: vc.profile_image,
color: color_int_to_hex_string(color),
timestamp: vc.timestamp.ok(),
}
}
}

View File

@@ -98,14 +98,6 @@ pub enum EventType {
contact_id: u32,
},
/// Incoming reaction, should be notified.
#[serde(rename_all = "camelCase")]
IncomingReaction {
contact_id: u32,
msg_id: u32,
reaction: String,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -248,15 +240,6 @@ pub enum EventType {
status_update_serial: u32,
},
/// Data received over an ephemeral peer channel.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
/// Advertisement received over an ephemeral peer channel.
/// This can be used by bots to initiate peer-to-peer communication from their side.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted { msg_id: u32 },
@@ -267,18 +250,6 @@ pub enum EventType {
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
ChatlistChanged,
/// Inform that a single chat list item changed and needs to be rerendered.
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow { n: u64 },
}
impl From<CoreEventType> for EventType {
@@ -310,15 +281,6 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
CoreEventType::IncomingReaction {
contact_id,
msg_id,
reaction,
} => IncomingReaction {
contact_id: contact_id.to_u32(),
msg_id: msg_id.to_u32(),
reaction: reaction.as_str().to_string(),
},
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
@@ -391,27 +353,10 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
msg_id: msg_id.to_u32(),
data,
},
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
WebxdcRealtimeAdvertisementReceived {
msg_id: msg_id.to_u32(),
}
}
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
chat_id: chat_id.map(|id| id.to_u32()),
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
}
}
}

View File

@@ -1,4 +1,3 @@
use crate::api::VcardContact;
use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
@@ -36,10 +35,6 @@ pub struct MessageObject {
parent_id: Option<u32>,
text: String,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -88,8 +83,6 @@ pub struct MessageObject {
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
@@ -112,10 +105,8 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
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::get_by_id(context, message.get_from_id())
.await
@@ -178,14 +169,7 @@ impl MessageObject {
Some(reactions.into())
};
let vcard_contacts: Vec<VcardContact> = message
.vcard_contacts(context)
.await?
.into_iter()
.map(Into::into)
.collect();
let message_object = MessageObject {
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
@@ -244,10 +228,7 @@ impl MessageObject {
download_state,
reactions,
vcard_contact: vcard_contacts.first().cloned(),
};
Ok(Some(message_object))
})
}
}
@@ -289,11 +270,6 @@ pub enum MessageViewtype {
/// Message is an webxdc instance.
Webxdc,
/// Message containing shared contacts represented as a vCard (virtual contact file)
/// with email addresses and possibly other fields.
/// Use `parse_vcard()` to retrieve them.
Vcard,
}
impl From<Viewtype> for MessageViewtype {
@@ -310,7 +286,6 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
}
}
}
@@ -329,7 +304,6 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,
}
}
}
@@ -368,14 +342,6 @@ pub enum SystemMessageType {
LocationOnly,
InvalidUnencryptedMail,
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
/// to complete.
SecurejoinWait,
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
/// send messages.
SecurejoinWaitTimeout,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
@@ -394,9 +360,6 @@ pub enum SystemMessageType {
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
/// This message contains a users iroh node address.
IrohNodeAddr,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -419,9 +382,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
}
}
}
@@ -493,7 +453,6 @@ pub struct MessageSearchResult {
author_name: String,
author_color: String,
author_id: u32,
chat_id: u32,
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
@@ -533,7 +492,6 @@ impl MessageSearchResult {
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(),
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
@@ -582,9 +540,7 @@ pub struct MessageData {
pub file: Option<String>,
pub location: Option<(f64, f64)>,
pub override_sender_name: Option<String>,
/// Quoted message id. Takes preference over `quoted_text` (see below).
pub quoted_message_id: Option<u32>,
pub quoted_text: Option<String>,
}
impl MessageData {
@@ -610,16 +566,16 @@ impl MessageData {
message.set_location(latitude, longitude);
}
if let Some(id) = self.quoted_message_id {
let quoted_message = Message::load_from_db(context, MsgId::new(id))
.await
.context("Failed to load quoted message")?;
message
.set_quote(context, Some(&quoted_message))
.await
.context("Failed to set quote")?;
} else if let Some(text) = self.quoted_text {
let protect = false;
message.set_quote_text(Some((text, protect)));
.set_quote(
context,
Some(
&Message::load_from_db(context, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
Ok(message)
}
@@ -642,7 +598,7 @@ pub struct MessageInfo {
error: Option<String>,
rfc724_mid: String,
server_urls: Vec<String>,
hop_info: String,
hop_info: Option<String>,
}
impl MessageInfo {
@@ -675,7 +631,7 @@ impl MessageInfo {
#[derive(
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum EphemeralTimer {
/// Timer is disabled.
Disabled,

View File

@@ -6,161 +6,80 @@ use typescript_type_def::TypeDef;
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
pub enum QrObject {
/// Ask the user whether to verify the contact.
///
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
AskVerifyContact {
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the group.
AskVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// ID of the contact.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
FprOk {
/// Contact ID.
contact_id: u32,
},
/// Scanned fingerprint does not match the last seen fingerprint.
FprMismatch {
/// Contact ID.
contact_id: Option<u32>,
},
/// The scanned QR code contains a fingerprint but no e-mail address.
FprWithoutAddr {
/// Key fingerprint.
fingerprint: String,
},
/// Ask the user if they want to create an account on the given domain.
Account {
/// Server domain name.
domain: String,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Authentication token.
auth_token: String,
/// Iroh node address.
node_addr: String,
Backup {
ticket: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
domain: String,
instance_pattern: String,
},
/// Ask the user if they want to use the given proxy.
///
/// Note that HTTP(S) URLs without a path
/// and query parameters are treated as HTTP(S) proxy URL.
/// UI may want to still offer to open the URL
/// in the browser if QR code contents
/// starts with `http://` or `https://`
/// and the QR code was not scanned from
/// the proxy configuration screen.
Proxy {
/// Proxy URL.
///
/// This is the URL that is going to be added.
url: String,
/// Host extracted from the URL to display in the UI.
host: String,
/// Port extracted from the URL to display in the UI.
port: u16,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
/// Ask the user if they want to start chatting.
Addr {
/// Contact ID.
contact_id: u32,
/// Draft message.
draft: Option<String>,
},
/// URL scanned.
///
/// Ask the user if they want to open a browser or copy the URL to clipboard.
Url { url: String },
/// Text scanned.
///
/// Ask the user if they want to copy the text to clipboard.
Text { text: String },
/// Ask the user if they want to withdraw their own QR code.
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to withdraw their own group invite QR code.
WithdrawVerifyGroup {
/// Group name.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own QR code.
ReviveVerifyContact {
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Ask the user if they want to revive their own group invite QR code.
ReviveVerifyGroup {
/// Contact ID.
grpname: String,
/// Group ID.
grpid: String,
/// Contact ID.
contact_id: u32,
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// `dclogin:` scheme parameters.
///
/// Ask the user if they want to login with the email address.
Login { address: String },
Login {
address: String,
},
}
impl From<Qr> for QrObject {
@@ -210,12 +129,8 @@ impl From<Qr> for QrObject {
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::Backup2 {
ref node_addr,
auth_token,
} => QrObject::Backup2 {
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
auth_token,
Qr::Backup { ticket } => QrObject::Backup {
ticket: ticket.to_string(),
},
Qr::WebrtcInstance {
domain,
@@ -224,7 +139,6 @@ impl From<Qr> for QrObject {
domain,
instance_pattern,
},
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }

View File

@@ -1,6 +1,3 @@
#![recursion_limit = "256"]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
pub mod api;
pub use yerpc;
@@ -85,7 +82,7 @@ mod tests {
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::net::SocketAddr;
use std::path::PathBuf;

View File

@@ -3,7 +3,7 @@
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"yerpc": "^0.6.2"
"yerpc": "^0.4.3"
},
"devDependencies": {
"@types/chai": "^4.2.21",
@@ -25,17 +25,12 @@
"exports": {
".": {
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs",
"types": "./dist/deltachat.d.ts"
"require": "./dist/deltachat.cjs"
}
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
@@ -58,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.149.0"
"version": "1.137.2"
}

View File

@@ -86,7 +86,10 @@ describe("online tests", function () {
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
const { chatId: chatIdOnAccountB } = await eventPromise;
@@ -116,7 +119,10 @@ describe("online tests", function () {
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
// wait for message from A
console.log("wait for message from A");
@@ -137,7 +143,10 @@ describe("online tests", function () {
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = waitForEvent(dc, "IncomingMsg", accountId1);
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arrives at A and if it is encrypted
await eventPromise2;

View File

@@ -1,21 +1,20 @@
[package]
name = "deltachat-repl"
version = "1.149.0"
version = "1.137.2"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
rusqlite = { workspace = true }
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rustyline = "14"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]
default = ["vendored"]

View File

@@ -19,10 +19,8 @@ use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
@@ -340,6 +338,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
receive-backup <qr>\n\
export-keys\n\
import-keys\n\
export-setup\n\
dump <filename>\n\n
read <filename>\n\n
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -356,7 +357,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
configure\n\
connect\n\
disconnect\n\
fetch\n\
connectivity\n\
maybenetwork\n\
housekeeping\n\
@@ -426,7 +426,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
checkqr <qr-content>\n\
joinqr <qr-content>\n\
setqr <qr-content>\n\
createqrsvg <qr-content>\n\
providerinfo <addr>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
@@ -489,9 +488,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"send-backup" => {
let provider = BackupProvider::prepare(&context).await?;
let qr = format_backup(&provider.qr())?;
println!("QR code: {}", qr);
qr2term::print_qr(qr.as_str())?;
let qr = provider.qr();
println!("QR code: {}", format_backup(&qr)?);
provider.await?;
}
"receive-backup" => {
@@ -507,6 +505,25 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(&context, &setup_code).await?;
fs::write(&file_name, file_content).await?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
&setup_code,
);
}
"dump" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
serialize_database(&context, arg1).await?;
}
"read" => {
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
deserialize_database(&context, arg1).await?;
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
@@ -635,30 +652,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
let msg_id = MsgId::new(arg1.parse()?);
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
if let Some(res) = res {
println!("waiting for peer channel join");
res.await?;
}
println!("joined peer channel");
}
"send-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
if arg2.is_empty() {
bail!("no message");
}
let msg_id = MsgId::new(arg1.parse()?);
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
println!("sent realtime message");
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
bail!("Argument [chat-id] is missing.");
@@ -1004,7 +997,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() {
let mut draft = Message::new_text(arg1.to_string());
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
sel_chat
.as_ref()
.unwrap()
@@ -1027,7 +1021,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new_text(arg1.to_string());
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1249,19 +1244,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Err(err) => println!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let svg = create_qr_svg(arg1)?;
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
fs::write(&file, svg).await?;
println!("{file:#?} written.");
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
.await?;
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
//! This is a CLI program and a little testing frame. This file must not be
//! included when using Delta Chat Core as a library.
//!
@@ -9,7 +8,10 @@
extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::process::Command;
use ansi_term::Color;
use anyhow::{bail, Error};
use deltachat::chat::ChatId;
use deltachat::config;
@@ -19,7 +21,6 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
@@ -30,7 +31,6 @@ use rustyline::{
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing_subscriber::EnvFilter;
mod cmdline;
use self::cmdline::*;
@@ -150,7 +150,7 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 13] = [
const IMEX_COMMANDS: [&str; 14] = [
"initiate-key-transfer",
"get-setupcodebegin",
"continue-key-transfer",
@@ -161,12 +161,13 @@ const IMEX_COMMANDS: [&str; 13] = [
"receive-backup",
"export-keys",
"import-keys",
"export-setup",
"poke",
"reset",
"stop",
];
const DB_COMMANDS: [&str; 11] = [
const DB_COMMANDS: [&str; 10] = [
"info",
"set",
"get",
@@ -174,7 +175,6 @@ const DB_COMMANDS: [&str; 11] = [
"configure",
"connect",
"disconnect",
"fetch",
"connectivity",
"maybenetwork",
"housekeeping",
@@ -240,13 +240,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 12] = [
const MISC_COMMANDS: [&str; 11] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"createqrsvg",
"fileinfo",
"clear",
"exit",
@@ -417,9 +416,6 @@ async fn handle_cmd(
"disconnect" => {
ctx.stop_io().await;
}
"fetch" => {
ctx.background_fetch().await?;
}
"configure" => {
ctx.configure().await?;
}
@@ -449,7 +445,12 @@ async fn handle_cmd(
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
let output = Command::new("qrencode")
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
}
}
"getqrsvg" => {
@@ -481,10 +482,9 @@ async fn handle_cmd(
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
)
pretty_env_logger::formatted_timed_builder()
.parse_default_env()
.filter_module("deltachat_repl", log::LevelFilter::Info)
.init();
let args = std::env::args().collect();

View File

@@ -4,7 +4,6 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()

View File

@@ -3,7 +3,6 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
import logging
import sys
from threading import Thread

View File

@@ -2,7 +2,6 @@
"""
Example echo bot without using hooks
"""
import logging
import sys

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.149.0"
version = "1.137.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -13,20 +13,14 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]
readme = "README.md"
dependencies = [
"imap-tools",
]
[tool.setuptools.package-data]
deltachat_rpc_client = [
@@ -40,7 +34,7 @@ deltachat_rpc_client = [
line-length = 120
[tool.ruff]
lint.select = [
select = [
"E", "W", # pycodestyle
"F", # Pyflakes
"N", # pep8-naming

View File

@@ -104,11 +104,7 @@ def _run_cli(
if not client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
configure_thread = Thread(
target=client.configure,
daemon=True,
kwargs={"email": args.email, "password": args.password},
)
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
configure_thread.start()
client.run_forever()

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from warnings import warn
from ._utils import AttrDict, futuremethod
@@ -30,10 +28,6 @@ class Account:
"""Wait until the next event and return it."""
return AttrDict(self._rpc.wait_for_event(self.id))
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
"""Remove the account."""
self._rpc.remove_account(self.id)
@@ -93,14 +87,6 @@ class Account:
"""Configure an account."""
yield self._rpc.configure.future(self.id)
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
while True:
event = self.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -118,11 +104,6 @@ class Account:
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
def create_chat(self, account: "Account") -> Chat:
addr = account.get_config("addr")
contact = self.create_contact(addr)
return contact.create_chat()
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
@@ -132,7 +113,7 @@ class Account:
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
def get_blocked_contacts(self) -> list[AttrDict]:
def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
@@ -157,7 +138,7 @@ class Account:
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[list[Contact], list[AttrDict]]:
) -> Union[List[Contact], List[AttrDict]]:
"""Get a filtered list of contacts.
:param query: if a string is specified, only return contacts
@@ -192,7 +173,7 @@ class Account:
no_specials: bool = False,
alldone_hint: bool = False,
snapshot: bool = False,
) -> Union[list[Chat], list[AttrDict]]:
) -> Union[List[Chat], List[AttrDict]]:
"""Return list of chats.
:param query: if a string is specified only chats matching this query are returned.
@@ -244,37 +225,33 @@ class Account:
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://securejoin.delta.chat/ for protocol details.
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> str:
"""Get Setup-Contact QR Code text.
def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
This data needs to be transferred to another Delta Chat account
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
return self._rpc.get_chat_securejoin_qr_code(self.id, None)
def get_qr_code_svg(self) -> tuple[str, str]:
"""Get Setup-Contact QR code text and SVG."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
def mark_seen_messages(self, messages: list[Message]) -> None:
def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
def delete_messages(self, messages: list[Message]) -> None:
def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
def get_fresh_messages(self) -> list[Message]:
def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
@@ -284,12 +261,12 @@ class Account:
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def get_next_messages(self) -> list[Message]:
def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_next_messages(self) -> list[Message]:
def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
@@ -301,12 +278,6 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event."""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
@@ -319,13 +290,7 @@ class Account:
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def wait_for_reactions_changed(self):
while True:
event = self.wait_for_event()
if event.kind == EventType.REACTIONS_CHANGED:
return event
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",

View File

@@ -1,9 +1,6 @@
from __future__ import annotations
import calendar
from dataclasses import dataclass
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from ._utils import AttrDict
from .const import ChatVisibility, ViewType
@@ -96,11 +93,7 @@ class Chat:
"""Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> str:
"""Get Join-Group QR code text."""
return self._rpc.get_chat_securejoin_qr_code(self.account.id, self.id)
def get_qr_code_svg(self) -> tuple[str, str]:
def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
@@ -124,7 +117,7 @@ class Chat:
html: Optional[str] = None,
viewtype: Optional[ViewType] = None,
file: Optional[str] = None,
location: Optional[tuple[float, float]] = None,
location: Optional[Tuple[float, float]] = None,
override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None,
) -> Message:
@@ -149,10 +142,6 @@ class Chat:
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
def send_file(self, path):
"""Send a file and return the resulting Message instance."""
return self.send_message(file=path)
def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
@@ -163,7 +152,7 @@ class Chat:
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def forward_messages(self, messages: list[Message]) -> None:
def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
@@ -195,7 +184,7 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
@@ -230,7 +219,7 @@ class Chat:
contact_id = cnt
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
def get_contacts(self) -> list[Contact]:
def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
@@ -254,7 +243,7 @@ class Chat:
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
timestamp_to: Optional["datetime"] = None,
) -> list[AttrDict]:
) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan."""
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
@@ -262,7 +251,7 @@ class Chat:
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: dict[int, Contact] = {}
contacts: Dict[int, Contact] = {}
for loc in result:
location = AttrDict(loc)
location["chat"] = self
@@ -270,11 +259,3 @@ class Chat:
location["message"] = Message(self.account, location.msg_id)
locations.append(location)
return locations
def send_contact(self, contact: Contact):
"""Send contact to the chat."""
vcard = contact.make_vcard()
with NamedTemporaryFile(suffix=".vcard") as f:
f.write(vcard.encode())
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})

View File

@@ -1,13 +1,14 @@
"""Event loop implementations offering high level event handling/hooking."""
from __future__ import annotations
import logging
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
Optional,
Set,
Tuple,
Type,
Union,
)
@@ -38,16 +39,16 @@ class Client:
def __init__(
self,
account: "Account",
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self.account = account
self.logger = logger or logging
self._hooks: dict[type, set[tuple]] = {}
self._hooks: Dict[type, Set[tuple]] = {}
self._should_process_messages = 0
self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
for hook, event in hooks:
self.add_hook(hook, event)

View File

@@ -59,11 +59,6 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
class ChatId(IntEnum):
@@ -116,7 +111,6 @@ class ViewType(str, Enum):
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
class SystemMessageType(str, Enum):

View File

@@ -36,10 +36,6 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)
@@ -64,6 +60,3 @@ class Contact:
self.account,
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)
def make_vcard(self) -> str:
return self._rpc.make_vcard(self.account.id, [self.id])

View File

@@ -1,6 +1,4 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, List
from ._utils import AttrDict
from .account import Account
@@ -23,7 +21,7 @@ class DeltaChat:
account_id = self.rpc.add_account()
return Account(self, account_id)
def get_all_accounts(self) -> list[Account]:
def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
account_ids = self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
@@ -46,6 +44,6 @@ class DeltaChat:
"""Get information about the Delta Chat core in this system."""
return AttrDict(self.rpc.get_system_info())
def set_translations(self, translations: dict[str, str]) -> None:
def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
self.rpc.set_stock_strings(translations)

View File

@@ -1,214 +0,0 @@
"""
Internal Python-level IMAP handling used by the tests.
"""
from __future__ import annotations
import imaplib
import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import TYPE_CHECKING
from imap_tools import (
AND,
Header,
MailBox,
MailMessage,
MailMessageFlags,
errors,
)
if TYPE_CHECKING:
from . import Account
FLAGS = b"FLAGS"
FETCH = b"FETCH"
ALL = "1:*"
class DirectImap:
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
def connect(self):
# Assume the testing server supports TLS on port 993.
host = self.account.get_config("configured_mail_server")
port = 993
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
self.conn.login(user, pw)
self.select_folder("INBOX")
def shutdown(self):
try:
self.conn.logout()
except (OSError, imaplib.IMAP4.abort):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.folder.create(foldername)
except errors.MailboxFolderCreateError as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername: str) -> tuple:
assert not self._idling
return self.conn.folder.set(foldername)
def select_config_folder(self, config_name: str):
"""Return info about selected folder if it is
configured, otherwise None.
"""
if "_" not in config_name:
config_name = f"configured_{config_name}_folder"
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
return None
def list_folders(self) -> list[str]:
"""return list of all existing folder names."""
assert not self._idling
return [folder.name for folder in self.conn.folder.list()]
def delete(self, uid_list: str, expunge=True):
"""delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not
just flagged as deleted.
"""
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
if expunge:
self.conn.expunge()
def get_all_messages(self) -> list[MailMessage]:
assert not self._idling
return list(self.conn.fetch())
def get_unread_messages(self) -> list[str]:
assert not self._idling
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
def mark_all_read(self):
messages = self.get_unread_messages()
if messages:
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
print("marked seen:", messages, res)
def get_unread_cnt(self) -> int:
return len(self.get_unread_messages())
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()
def log(*args, **kwargs):
kwargs["file"] = stream
print(*args, **kwargs)
empty_folders = []
for imapfolder in self.list_folders():
self.select_folder(imapfolder)
messages = list(self.get_all_messages())
if not messages:
empty_folders.append(imapfolder)
continue
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
for msg in self.conn.fetch(mark_seen=False):
body = getattr(msg.obj, "text", None)
if not body:
body = getattr(msg.obj, "html", None)
if not body:
log("Message", msg.uid, "has empty body")
continue
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
fn = path.joinpath(str(msg.uid))
fn.write_bytes(body)
log("Message", msg.uid, fn)
log(
"Message",
msg.uid,
msg.flags,
"Message-Id:",
msg.obj.get("Message-Id"),
)
if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders)
print(stream.getvalue(), file=logfile)
@contextmanager
def idle(self):
"""return Idle ContextManager."""
idle_manager = IdleManager(self)
try:
yield idle_manager
finally:
idle_manager.done()
def append(self, folder: str, msg: str):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> list[bytes]:
"""(blocking) wait for next idle message from server."""
self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log(f"imap-direct: idle_check returned {res!r}")
return res
def wait_for_new_message(self, timeout=None) -> bytes:
while True:
for item in self.check(timeout=timeout):
if b"EXISTS" in item or b"RECENT" in item:
return item
def wait_for_seen(self, timeout=None) -> int:
"""Return first message with SEEN flag from a running idle-stream."""
while True:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))
if FLAGS in item and rb"\Seen" in item:
return int(item.split(b" ")[1])
def done(self):
"""send idle-done to server if we are currently in idle mode."""
return self.direct_imap.conn.idle.stop()

View File

@@ -1,10 +1,8 @@
"""High-level classes for event processing and filtering."""
from __future__ import annotations
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from .const import EventType
@@ -265,9 +263,9 @@ class HookCollection:
"""
def __init__(self) -> None:
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
return iter(self._hooks)
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa

View File

@@ -2,8 +2,7 @@ import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict, futuremethod
from .const import EventType
from ._utils import AttrDict
from .contact import Contact
if TYPE_CHECKING:
@@ -22,10 +21,9 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
def send_reaction(self, *reaction: str) -> "Message":
def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
return Message(self.account, msg_id)
self._rpc.send_reaction(self.account.id, self.id, reaction)
def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
@@ -63,18 +61,3 @@ class Message:
def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
"""Consume events until the message is delivered."""
while True:
event = self.account.wait_for_event()
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break
@futuremethod
def send_webxdc_realtime_advertisement(self):
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data) -> None:
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))

View File

@@ -1,12 +1,10 @@
from __future__ import annotations
import os
import random
from typing import AsyncGenerator, Optional
from typing import AsyncGenerator, List, Optional
import pytest
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from ._utils import futuremethod
from .rpc import Rpc
@@ -56,10 +54,14 @@ class ACFactory:
@futuremethod
def get_online_account(self):
account = yield self.new_configured_account.future()
account.bring_online()
account.start_io()
while True:
event = account.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
return account
def get_online_accounts(self, num: int) -> list[Account]:
def get_online_accounts(self, num: int) -> List[Account]:
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
@@ -73,10 +75,6 @@ class ACFactory:
ac_clone.configure()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def send_message(
self,
to_account: Account,
@@ -114,13 +112,13 @@ class ACFactory:
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
@pytest.fixture
@pytest.fixture()
def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@pytest.fixture
@pytest.fixture()
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))

View File

@@ -1,14 +1,12 @@
from __future__ import annotations
import itertools
import json
import logging
import os
import subprocess
import sys
from queue import Empty, Queue
from queue import Queue
from threading import Event, Thread
from typing import Any, Iterator, Optional
from typing import Any, Dict, Iterator, Optional
class JsonRpcError(Exception):
@@ -69,11 +67,11 @@ class Rpc:
self._kwargs = kwargs
self.process: subprocess.Popen
self.id_iterator: Iterator[int]
self.event_queues: dict[int, Queue]
self.event_queues: Dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: dict[int, Event]
self.request_events: Dict[int, Event]
# Map from request ID to the result.
self.request_results: dict[int, Any]
self.request_results: Dict[int, Any]
self.request_queue: Queue[Any]
self.closing: bool
self.reader_thread: Thread
@@ -188,14 +186,5 @@ class Rpc:
queue = self.get_queue(account_id)
return queue.get()
def clear_all_events(self, account_id: int):
"""Removes all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:
queue.get_nowait()
except Empty:
pass
def __getattr__(self, attr: str):
return RpcMethod(self, attr)

View File

@@ -1,218 +0,0 @@
from __future__ import annotations
import base64
import os
from typing import TYPE_CHECKING
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
first_event = ""
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
first_event = "change"
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
first_event = "item_change"
break
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
break
def wait_for_chatlist_specific_item(account, chat_id):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
break
def wait_for_chatlist(account):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
break
def test_delivery_status(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice.clear_all_events()
bob.stop_io()
alice.stop_io()
alice_chat_bob.send_text("hi")
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
alice.clear_all_events()
alice.start_io()
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
bob.clear_all_events()
bob.start_io()
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
msg.get_snapshot().chat.accept()
msg.mark_seen()
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
alice.clear_all_events()
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_READ:
break
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acfactory.get_online_accounts(1)
invalid_contact = alice.create_contact("example@example.com", "invalid address")
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
alice.clear_all_events()
failing_message = invalid_chat.send_text("test")
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_FAILED:
break
wait_for_chatlist_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
alice.set_config("download_limit", "1")
msg = bob.wait_for_incoming_msg()
chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
)
message = alice.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
snapshot = message.get_snapshot()
chat_id = snapshot.chat_id
alice._rpc.download_full_message(alice.id, message.id)
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
bob.wait_for_incoming_msg_event()
alice_second_device: Account = acfactory.get_unconfigured_account()
alice._rpc.provide_backup.future(alice.id)
backup_code = alice._rpc.get_backup_qr(alice.id)
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
alice_second_device.start_io()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.clear_all_events()
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.send_text("hello")
msg = bob.wait_for_incoming_msg()
bob_chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.get_chat_by_id(bob_chat_id).send_text("hello")
# make sure alice_second_device already received the message
alice_second_device.wait_for_incoming_msg_event()
msg = alice.wait_for_incoming_msg()
alice_second_device.clear_all_events()
msg.mark_seen()
wait_for_chatlist_specific_item(bob, bob_chat_id)
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
alice_second_device.clear_all_events()
alice_chat_bob.pin()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().pinned
alice_second_device.clear_all_events()
alice_chat_bob.mute()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted

View File

@@ -1,236 +0,0 @@
#!/usr/bin/env python3
"""
Testing webxdc iroh connectivity
If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import logging
import os
import threading
import time
import pytest
from deltachat_rpc_client import EventType
@pytest.fixture
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
assert p.exists()
return str(p)
def log(msg):
logging.info(msg)
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "play"
# send iroh announcements simultaneously
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
return ac1_webxdc_msg, ac2_webxdc_msg
def setup_thread_send_realtime_data(msg, data):
def thread_run():
for _i in range(10):
msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
def wait_receive_realtime_data(msg_data_list):
account = msg_data_list[0][0].account
msg_data_list = msg_data_list[:]
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
while msg_data_list:
event = account.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
for i, (msg, data) in enumerate(msg_data_list):
if msg.id == event.msg_id:
assert list(data) == event.data
log(f"msg {msg.id}: got correct realtime data {data}")
del msg_data_list[i]
break
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping1"
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
# Test that 128 KB of data can be sent in a single message.
data = os.urandom(128000)
ac1_webxdc_msg.send_webxdc_realtime_data(data)
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(data)
break
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
ac2_webxdc_msg.get_snapshot().chat.accept()
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
# Issue a "send" call in parallel with sending advertisement.
# Previously due to a bug this caused subscribing to the channel twice.
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
def thread_run():
for i in range(10):
data = str(i).encode()
ac1_webxdc_msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
n = int(bytes(event.data).decode())
break
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert int(bytes(event.data).decode()) > n
break
def test_no_reordering(acfactory, path_to_webxdc):
"""Test that sending a lot of realtime messages does not result in reordering."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
for i in range(200):
ac1_webxdc_msg.send_webxdc_realtime_data([i])
for i in range(200):
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
if event.data[0] == i:
break
pytest.fail("Reordering detected")
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
"""Test that realtime advertisement is assigned to the correct message after chatting."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
ac1_ac2_chat.send_text("Hello!")
ac2_hello_msg = ac2.wait_for_incoming_msg()
ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot()
assert ac2_hello_msg_snapshot.text == "Hello!"
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
while 1:
event = ac1.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
assert event.msg_id == ac1_webxdc_msg.id
break

View File

@@ -1,15 +1,13 @@
import logging
import time
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
@@ -32,45 +30,23 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
bob2.export_self_keys(tmp_path)
logging.info("Bob imports a key")
bob.import_self_keys(tmp_path)
bob.import_self_keys(tmp_path / "private-key-default.asc")
assert bob.get_config("key_id") == "2"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert not bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acfactory) -> None:
alice = acfactory.new_configured_account()
_, _, domain = alice.get_config("addr").rpartition("@")
_qr_code, svg = alice.get_qr_code_svg()
alice.set_config("displayname", "Alice")
# Test that display name is used
# in SVG and no address is visible.
_qr_code, svg = alice.get_qr_code_svg()
assert domain not in svg
assert "Alice" in svg
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect, tmp_path):
alice, bob, fiona = acfactory.get_online_accounts(3)
def test_qr_securejoin(acfactory, protect):
alice, bob = acfactory.get_online_accounts(2)
# Setup second device for Alice
# to test observing securejoin protocol.
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
# Check that at least some of the handshake messages are deleted.
@@ -98,21 +74,6 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
# Start second Alice device.
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("Fiona joins the group via alice2")
alice.stop_io()
fiona.secure_join(qr_code)
alice2.wait_for_securejoin_inviter_success()
fiona.wait_for_securejoin_joiner_success()
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
@@ -130,7 +91,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = bob.wait_for_event()
@@ -145,7 +106,7 @@ def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
charlie.secure_join(qr_code)
@@ -207,13 +168,13 @@ def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
@@ -227,7 +188,7 @@ def test_verified_group_recovery(acfactory) -> None:
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -244,7 +205,7 @@ def test_verified_group_recovery(acfactory) -> None:
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -291,7 +252,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -308,7 +269,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -326,6 +287,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
@@ -335,8 +297,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
ac3_chat.add_contact(ac3_contact_ac2)
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
@@ -376,7 +336,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code = ac2.get_qr_code()
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
@@ -386,7 +346,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code = ch1.get_qr_code()
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -399,7 +359,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code = ch1.get_qr_code()
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
@@ -421,7 +381,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code = vg.get_qr_code()
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
@@ -442,7 +402,7 @@ def test_qr_new_group_unblocked(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code = ac1_chat.get_qr_code()
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -460,19 +420,15 @@ def test_qr_new_group_unblocked(acfactory):
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
# ac1new is only used to get a new address.
ac1new = acfactory.new_preconfigured_account()
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code = chat.get_qr_code()
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
@@ -508,12 +464,12 @@ def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# Bob verifies Alice.
qr_code = alice.get_qr_code()
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code = carol.get_qr_code()
qr_code, _svg = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
@@ -564,17 +520,16 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac3_chat = ac3.create_group("Verified group", protect=True)
# ac1 joins ac3 group.
ac3_qr_code = ac3_chat.get_qr_code()
ac3_qr_code, _svg = ac3_chat.get_qr_code()
ac1.secure_join(ac3_qr_code)
ac1.wait_for_securejoin_joiner_success()
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
# ac2 verifies ac1
qr_code = ac1.get_qr_code()
qr_code, _svg = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -585,29 +540,17 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
# Loop sending message from ac1 to ac2
# until ac2 accepts new ac1 key.
#
# This may not happen immediately because resetup of ac1
# rewinds "smeared timestamp" so Date: header for messages
# sent by new ac1 are in the past compared to the last Date:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
logging.info("ac2 received Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
if not ac2_contact_ac1.get_snapshot().is_verified:
break
time.sleep(1)
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
@@ -646,7 +589,7 @@ def test_withdraw_securejoin_qr(acfactory):
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
@@ -669,8 +612,7 @@ def test_withdraw_securejoin_qr(acfactory):
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if (
event.kind == EventType.WARNING
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
):
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

@@ -1,18 +1,10 @@
import base64
import concurrent.futures
import json
import logging
import os
import socket
import subprocess
import time
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@@ -57,8 +49,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
logging.info(event)
logging.info("Successful configuration")
print(event)
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
@@ -71,38 +63,6 @@ def test_configure_starttls(acfactory) -> None:
assert account.is_configured()
def test_configure_ip(acfactory) -> None:
account = acfactory.new_preconfigured_account()
domain = account.get_config("addr").rsplit("@")[-1]
ip_address = socket.gethostbyname(domain)
# This should fail TLS check.
account.set_config("mail_server", ip_address)
with pytest.raises(JsonRpcError):
account.configure()
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
account = acfactory.new_preconfigured_account()
account.set_config("mail_port", "443")
account.set_config("send_port", "443")
account.configure()
def test_configure_username(acfactory) -> None:
account = acfactory.new_preconfigured_account()
addr = account.get_config("addr")
account.set_config("mail_user", addr)
account.configure()
assert account.get_config("configured_mail_user") == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -138,12 +98,12 @@ def test_account(acfactory) -> None:
assert alice.get_chatlist(snapshot=True)
assert alice.get_qr_code()
assert alice.get_fresh_messages()
assert alice.get_next_messages()
# Test sending empty message.
assert len(bob.wait_next_messages()) == 0
alice_chat_bob.send_text("")
messages = bob.wait_next_messages()
assert bob.get_next_messages() == messages
assert len(messages) == 1
message = messages[0]
snapshot = message.get_snapshot()
@@ -246,7 +206,6 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
@@ -434,7 +393,7 @@ def test_provider_info(rpc) -> None:
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
@@ -480,200 +439,3 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
message = alice.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.show_padlock
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
logging.info("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
for m in msgs:
m.wait_until_delivered()
logging.info("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
msgs[-1].wait_until_delivered()
ac2.start_io()
logging.info("wait for ac2 to receive a reaction")
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1_addr
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
ac2.set_config("mvbox_move", "1")
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
for account in others:
chat = account.create_chat(alice)
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
contact_addr = account.get_config("addr")
contact = alice.create_contact(contact_addr, "")
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "hi"
bob_group = snapshot.chat
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
for i in range(10):
logging.info("Sending message %s", i)
alice_group.send_file(str(path))
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory, tmp_path):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
bob2 = acfactory.get_unconfigured_account()
bob2.import_backup(files[0])
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
while True:
event = bob2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
assert configured_certificate_checks
# 0 is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
#
# New versions of Delta Chat are not disabling TLS checks
# unless users explicitly disables them
# or provider database says provider has invalid certificates.
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert configured_certificate_checks != "0"

View File

@@ -1,15 +0,0 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"

View File

@@ -22,9 +22,10 @@ skipsdist = True
skip_install = True
deps =
ruff
black
commands =
ruff format --quiet --diff src/ examples/ tests/
ruff check src/ examples/ tests/
black --quiet --check --diff src/ examples/ tests/
ruff src/ examples/ tests/
[pytest]
timeout = 300

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.149.0"
version = "1.137.2"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -10,18 +10,18 @@ keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[dependencies]
deltachat-jsonrpc = { workspace = true }
deltachat = { workspace = true }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = { workspace = true }
futures-lite = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["io-std"] }
tokio-util = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "2.3.0"
log = "0.4"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.37.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]
default = ["vendored"]

View File

@@ -1,3 +0,0 @@
platform_package
*.tgz
package-lock.json

View File

@@ -1,3 +0,0 @@
platform_package/*
scripts/
*.tgz

View File

@@ -1,78 +0,0 @@
## npm package for deltachat-rpc-server
This is the successor of `deltachat-node`,
it does not use NAPI bindings but instead uses stdio executables
to let you talk to core over jsonrpc over stdio.
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
## Usage
> The **minimum** nodejs version for this package is `16`
```
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
```
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close()
}
main()
```
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
## How to use on an unsupported platform
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
## How does it work when you install it
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
references:
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
When you import this package it searches for the rpc server in the following locations and order:
1. `DELTA_CHAT_RPC_SERVER` environment variable
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
3. prebuilds in npm packages
so by default it uses the prebuilds.
## How do you built this package in CI
- To build platform packages, run the `build_platform_package.py` script:
```
python3 build_platform_package.py <cargo-target>
# example
python3 build_platform_package.py x86_64-apple-darwin
```
- Then pass it as an artifact to the last CI action that publishes the main package.
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
- then publish `deltachat-rpc-server/npm-package`,
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
## How to build a version you can use localy on your host machine for development
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
- If you just need your host platform run `python scripts/make_local_dev_version.py`
- note: this clears the `platform_package` folder
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
## Thanks to nlnet
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.

View File

@@ -1,42 +0,0 @@
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
export interface SearchOptions {
/** whether take deltachat-rpc-server inside of $PATH*/
takeVersionFromPATH: boolean;
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
disableEnvPath: boolean;
}
/**
*
* @returns absolute path to deltachat-rpc-server binary
* @throws when it is not found
*/
export function getRPCServerPath(
options?: Partial<SearchOptions>
): Promise<string>;
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
readonly pathToServerBinary: string;
};
export interface StartOptions {
/** whether to disable outputting stderr to the parent process's stderr */
muteStdErr: boolean;
}
/**
*
* @param directory directory for accounts folder
* @param options
*/
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
export namespace FnTypes {
export type getRPCServerPath = typeof getRPCServerPath
export type startDeltaChat = typeof startDeltaChat
}

View File

@@ -1,108 +0,0 @@
//@ts-check
import { spawn } from "node:child_process";
import { stat } from "node:fs/promises";
import os from "node:os";
import process from "node:process";
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
import {
ENV_VAR_LOCATION_NOT_FOUND,
FAILED_TO_START_SERVER_EXECUTABLE,
NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR,
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
} from "./src/errors.js";
import { createRequire } from "node:module";
function findRPCServerInNodeModules() {
const arch = os.arch();
const operating_system = process.platform;
const package_name = `@deltachat/stdio-rpc-server-${operating_system}-${arch}`;
try {
const { resolve } = createRequire(import.meta.url);
return resolve(package_name);
} catch (error) {
console.debug("findRpcServerInNodeModules", error);
const require = createRequire(import.meta.url);
if (
Object.keys(require("./package.json").optionalDependencies).includes(
package_name
)
) {
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
} else {
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
}
}
}
/** @type {import("./index").FnTypes.getRPCServerPath} */
export async function getRPCServerPath(options = {}) {
const { takeVersionFromPATH, disableEnvPath } = {
takeVersionFromPATH: false,
disableEnvPath: false,
...options,
};
// 1. check if it is set as env var
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
try {
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
throw new Error(
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
);
}
} catch (error) {
throw new Error(ENV_VAR_LOCATION_NOT_FOUND());
}
return process.env[ENV_VAR_NAME];
}
// 2. check if PATH should be used
if (takeVersionFromPATH) {
return PATH_EXECUTABLE_NAME;
}
// 3. check for prebuilds
return findRPCServerInNodeModules();
}
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
/** @type {import("./index").FnTypes.startDeltaChat} */
export async function startDeltaChat(directory, options = {}) {
const pathToServerBinary = await getRPCServerPath(options);
const server = spawn(pathToServerBinary, {
env: {
RUST_LOG: process.env.RUST_LOG,
DC_ACCOUNTS_PATH: directory,
},
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
});
server.on("error", (err) => {
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
//@ts-expect-error
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
dc.close = () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
};
//@ts-expect-error
dc.pathToServerBinary = pathToServerBinary;
return dc;
}

View File

@@ -1,19 +0,0 @@
{
"license": "MPL-2.0",
"main": "index.js",
"name": "@deltachat/stdio-rpc-server",
"optionalDependencies": {},
"peerDependencies": {
"@deltachat/jsonrpc-client": "*"
},
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"prepack": "node scripts/update_optional_dependencies_and_version.js"
},
"type": "module",
"types": "index.d.ts",
"version": "1.149.0"
}

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env python3
import subprocess
from sys import argv
from os import path, makedirs, chdir
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 2:
print("First argument should be target architecture as required by cargo")
exit(1)
target = argv[1].strip()
subprocess.run(
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
check=True,
)
newpath = "platform_package"
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = "platform_package/" + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
def binary_path(binary_name):
return "../../target/" + target + "/release/" + binary_name
my_binary_name = "deltachat-rpc-server"
if not path.isfile(binary_path("deltachat-rpc-server")):
my_binary_name = "deltachat-rpc-server.exe"
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
print("Did not find the build")
exit(1)
my_binary_path = binary_path(my_binary_name)
copy(my_binary_path, platform_path + "/" + my_binary_name)
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -1,34 +0,0 @@
# This script is for making a version of the npm packet that you can install locally
import subprocess
from sys import argv
from os import path, makedirs, chdir
import re
import json
import tomllib
from shutil import copy, rmtree
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
# get host target with "rustc -vV"
output = subprocess.run(["rustc", "-vV"], capture_output=True)
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
print("host target to build for is:", host_target)
# clean platform_package folder
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
else:
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
makedirs(newpath)
# run build_platform_package.py with the host's target to build it
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)
# typescript / npm local package installing/linking needs that this package has it's own node_modules folder
subprocess.run(["npm", "i"], capture_output=False, check=True)

View File

@@ -1,46 +0,0 @@
import subprocess
from sys import argv
from os import path, makedirs, chdir, chmod, stat
import json
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 3:
print("First argument should be target architecture as required by cargo")
print("Second argument should be the location of th built binary (binary_path)")
exit(1)
target = argv[1].strip()
binary_path = argv[2].strip()
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
available_targets = output.stdout.decode("utf-8")
if available_targets.find(target) == -1:
print("target", target, "is not known / not valid")
exit(1)
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = 'platform_package/' + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
my_binary_name = path.basename(binary_path)
new_binary_path = platform_path + "/" + my_binary_name
copy(binary_path, new_binary_path)
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -1,21 +0,0 @@
def convert_cpu_arch_to_npm_cpu_arch(arch):
if arch == "x86_64":
return "x64"
if arch == "i686":
return "ia32"
if arch == "aarch64":
return "arm64"
if arch == "armv7" or arch == "arm":
return "arm"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
return arch
def convert_os_to_npm_os(os):
if os == "windows":
return "win32"
if os == "darwin" or os == "linux":
return os
if os.startswith("android"):
return "android"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
return os

View File

@@ -1,34 +0,0 @@
import tomllib
import json
from .convert_platform import convert_cpu_arch_to_npm_cpu_arch, convert_os_to_npm_os
def write_package_json(platform_path, rust_target, my_binary_name):
if len(rust_target.split("-")) == 3:
[cpu_arch, vendor, os] = rust_target.split("-")
else:
[cpu_arch, vendor, os, _env] = rust_target.split("-")
# read version
tomlfile = open("../../Cargo.toml", 'rb')
version = tomllib.load(tomlfile)['package']['version']
package_json = {
"name": "@deltachat/stdio-rpc-server-"
+ convert_os_to_npm_os(os)
+ "-"
+ convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
"version": version,
"os": [convert_os_to_npm_os(os)],
"cpu": [convert_cpu_arch_to_npm_cpu_arch(cpu_arch)],
"main": my_binary_name,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git",
},
}
file = open(platform_path + "/package.json", 'w')
file.write(json.dumps(package_json, indent=4))

View File

@@ -1,64 +0,0 @@
import fs from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
if (process.cwd() !== expected_cwd) {
console.error(
"CWD missmatch: this script needs to be run from " + expected_cwd,
{ actual: process.cwd(), expected: expected_cwd }
);
process.exit(1);
}
// whether to use local paths instead of npm registry version number for the prebuilds in optionalDependencies
// useful for local development
const is_local = process.argv.includes("--local");
const package_json = JSON.parse(await fs.readFile("./package.json", "utf8"));
const cargo_toml = await fs.readFile("../Cargo.toml", "utf8");
const version = cargo_toml
.split("\n")
.find((line) => line.includes("version"))
.split('"')[1];
const platform_packages_dir = "./platform_package";
const platform_package_names = await Promise.all(
(await fs.readdir(platform_packages_dir)).map(async (name) => {
const p = JSON.parse(
await fs.readFile(
join(platform_packages_dir, name, "package.json"),
"utf8"
)
);
if (p.version !== version) {
console.error(
name,
"has a different version than the version of the rpc server.",
{ rpc_server: version, platform_package: p.version }
);
throw new Error("version missmatch");
}
return { folder_name: name, package_name: p.name };
})
);
package_json.version = version;
package_json.optionalDependencies = {};
for (const { folder_name, package_name } of platform_package_names) {
package_json.optionalDependencies[package_name] = is_local
? `file:${expected_cwd}/platform_package/${folder_name}` // npm seems to work better with an absolute path here
: version;
}
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
}
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));

View File

@@ -1,5 +0,0 @@
//@ts-check
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"

View File

@@ -1,41 +0,0 @@
//@ts-check
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:
- Install it with "npm i ${package_name}"
- or download/compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR() {
return `deltachat-rpc-server not found:
Unfortunately no prebuild is available for your system, so you need to provide deltachat-rpc-server yourself.
- Download or Compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function ENV_VAR_LOCATION_NOT_FOUND(error) {
return `deltachat-rpc-server not found in ${ENV_VAR_NAME}:
Error: ${error}
Content of ${ENV_VAR_NAME}: "${process.env[ENV_VAR_NAME]}"`;
}
export function FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, error) {
return `Failed to start server executable at '${pathToServerBinary}',
Error: ${error}
Make sure the deltachat-rpc-server binary exists at this location
and you can start it with \`${pathToServerBinary} --version\``;
}

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
//! Delta Chat core RPC server.
//!
//! It speaks JSON Lines over stdio.
@@ -11,7 +10,6 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tracing_subscriber::EnvFilter;
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
@@ -29,9 +27,6 @@ async fn main() {
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
if let Err(error) = &r {
log::error!("Fatal error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
@@ -64,13 +59,7 @@ async fn main_impl() -> Result<()> {
#[cfg(target_family = "unix")]
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interferring with JSON-RPC using stdout.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
@@ -79,7 +68,7 @@ async fn main_impl() -> Result<()> {
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));
let state = CommandApi::from_arc(accounts.clone()).await;
let state = CommandApi::from_arc(accounts.clone());
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state.clone());

View File

@@ -1,6 +1,7 @@
[advisories]
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
@@ -9,15 +10,11 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Unmaintained proc-macro-error
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
"RUSTSEC-2024-0370",
# Unmaintained instant
"RUSTSEC-2024-0384",
]
[bans]
@@ -27,20 +24,24 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "fiat-crypto", version = "0.1.20" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http", version = "0.2.12" },
{ name = "nix", version = "0.26.4" },
{ name = "num_enum_derive", version = "0.5.11" },
{ name = "num_enum", version = "0.5.11" },
{ name = "proc-macro-crate", version = "1.3.1" },
{ name = "idna", version = "0.4.0" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
@@ -48,24 +49,29 @@ skip = [
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "ring", version = "0.16.20" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.19.15" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
{ name = "windows-core", version = "<0.54.0" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.59" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
]

114
flake.lock generated
View File

@@ -3,15 +3,15 @@
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"lastModified": 1710633978,
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
"type": "github"
},
"original": {
@@ -22,17 +22,18 @@
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"lastModified": 1708939976,
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
"type": "github"
},
"original": {
@@ -47,11 +48,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"lastModified": 1710742993,
"narHash": "sha256-W0PQCe0bW3hKF5lHawXrKynBcdSP18Qa4sb8DcUfOqI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"rev": "6f2fec850f569d61562d3a47dc263f19e9c7d825",
"type": "github"
},
"original": {
@@ -65,11 +66,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -83,11 +84,29 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -101,11 +120,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
@@ -116,11 +135,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -131,11 +150,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"type": "github"
},
"original": {
@@ -147,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"type": "github"
},
"original": {
@@ -163,10 +182,12 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"type": "path"
"lastModified": 1710765496,
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7",
"type": "github"
},
"original": {
"id": "nixpkgs",
@@ -175,11 +196,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"type": "github"
},
"original": {
@@ -193,7 +214,7 @@
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils_3",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
@@ -202,11 +223,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"lastModified": 1710708100,
"narHash": "sha256-Jd6pmXlwKk5uYcjyO/8BfbUVmx8g31Qfk7auI2IG66A=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"rev": "b6d1887bc4f9543b6c6bf098179a62446f34a6c3",
"type": "github"
},
"original": {
@@ -245,6 +266,21 @@
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -18,9 +18,9 @@
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
rustSrc = nix-filter.lib {
root = ./.;
@@ -35,7 +35,6 @@
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-contact-tools
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
@@ -257,21 +256,13 @@
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi21-clang";
cc = "armv7a-linux-androideabi19-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
x86 = {
cc = "i686-linux-android21-clang";
rustTarget = "i686-linux-android";
};
x86_64 = {
cc = "x86_64-linux-android21-clang";
rustTarget = "x86_64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
@@ -491,7 +482,6 @@
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
@@ -533,30 +523,20 @@
};
};
devShells.default =
let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in
pkgs.mkShell {
buildInputs = with pkgs; [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
(fenixPkgs.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
cargo-deny
fenixPkgs.rust-analyzer
perl # needed to build vendored OpenSSL
];
};
}
);
}

View File

@@ -8,8 +8,6 @@
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
/// Wraps line to 72 characters using format=flowed soft breaks.
///

2695
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
// Generated!
module.exports = {
DC_CERTCK_ACCEPT_INVALID: 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
DC_CERTCK_AUTO: 0,
DC_CERTCK_STRICT: 1,
@@ -31,9 +30,6 @@ module.exports = {
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_CHANNEL_OVERFLOW: 2400,
DC_EVENT_CHATLIST_CHANGED: 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
DC_EVENT_CHAT_MODIFIED: 2020,
DC_EVENT_CONFIGURE_PROGRESS: 2041,
@@ -51,7 +47,6 @@ module.exports = {
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INCOMING_REACTION: 2002,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
@@ -69,8 +64,6 @@ module.exports = {
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT: 2151,
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
@@ -115,7 +108,6 @@ module.exports = {
DC_MSG_IMAGE: 20,
DC_MSG_STICKER: 23,
DC_MSG_TEXT: 10,
DC_MSG_VCARD: 90,
DC_MSG_VIDEO: 50,
DC_MSG_VIDEOCHAT_INVITATION: 70,
DC_MSG_VOICE: 41,
@@ -131,13 +123,11 @@ module.exports = {
DC_QR_ASK_VERIFYCONTACT: 200,
DC_QR_ASK_VERIFYGROUP: 202,
DC_QR_BACKUP: 251,
DC_QR_BACKUP2: 252,
DC_QR_ERROR: 400,
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_LOGIN: 520,
DC_QR_PROXY: 271,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_TEXT: 330,
@@ -181,7 +171,6 @@ module.exports = {
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
DC_STR_CONTACT: 200,
DC_STR_CONTACT_NOT_VERIFIED: 36,
DC_STR_CONTACT_SETUP_CHANGED: 37,
DC_STR_CONTACT_VERIFIED: 35,
@@ -275,8 +264,6 @@ module.exports = {
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECUREJOIN_WAIT: 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT: 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
DC_STR_SECURE_JOIN_REPLIES: 118,
DC_STR_SECURE_JOIN_STARTED: 117,

View File

@@ -16,7 +16,6 @@ module.exports = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -38,10 +37,5 @@ module.exports = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE'
}

View File

@@ -1,7 +1,6 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID = 2,
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
@@ -31,9 +30,6 @@ export enum C {
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_CHANNEL_OVERFLOW = 2400,
DC_EVENT_CHATLIST_CHANGED = 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
DC_EVENT_CHAT_MODIFIED = 2020,
DC_EVENT_CONFIGURE_PROGRESS = 2041,
@@ -51,7 +47,6 @@ export enum C {
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INCOMING_REACTION = 2002,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -69,8 +64,6 @@ export enum C {
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
@@ -115,7 +108,6 @@ export enum C {
DC_MSG_IMAGE = 20,
DC_MSG_STICKER = 23,
DC_MSG_TEXT = 10,
DC_MSG_VCARD = 90,
DC_MSG_VIDEO = 50,
DC_MSG_VIDEOCHAT_INVITATION = 70,
DC_MSG_VOICE = 41,
@@ -131,13 +123,11 @@ export enum C {
DC_QR_ASK_VERIFYCONTACT = 200,
DC_QR_ASK_VERIFYGROUP = 202,
DC_QR_BACKUP = 251,
DC_QR_BACKUP2 = 252,
DC_QR_ERROR = 400,
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_LOGIN = 520,
DC_QR_PROXY = 271,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_TEXT = 330,
@@ -181,7 +171,6 @@ export enum C {
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT = 200,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
@@ -275,8 +264,6 @@ export enum C {
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECUREJOIN_WAIT = 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT = 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
@@ -324,7 +311,6 @@ export const EventId2EventName: { [key: number]: string } = {
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2002: 'DC_EVENT_INCOMING_REACTION',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
@@ -346,10 +332,5 @@ export const EventId2EventName: { [key: number]: string } = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
}

View File

@@ -475,6 +475,47 @@ export class Context extends EventEmitter {
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
}
getNextMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
}
getPreviousMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
}
_getNextMedia(
messageId: number,
dir: number,
msgType1: number,
msgType2: number,
msgType3: number
): number {
return binding.dcn_get_next_media(
this.dcn_context,
Number(messageId),
dir,
msgType1 || 0,
msgType2 || 0,
msgType3 || 0
)
}
getSecurejoinQrCode(chatId: number): string {
debug(`getSecurejoinQrCode ${chatId}`)
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))

View File

@@ -1,4 +1,5 @@
#define NAPI_VERSION 4
#define NAPI_EXPERIMENTAL
#include <stdlib.h>
#include <stdio.h>
@@ -1053,6 +1054,27 @@ NAPI_METHOD(dcn_get_msg_html) {
NAPI_RETURN_AND_UNREF_STRING(msg_html);
}
NAPI_METHOD(dcn_get_next_media) {
NAPI_ARGV(6);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(msg_id, 1);
NAPI_ARGV_INT32(dir, 2);
NAPI_ARGV_INT32(msg_type1, 3);
NAPI_ARGV_INT32(msg_type2, 4);
NAPI_ARGV_INT32(msg_type3, 5);
//TRACE("calling..");
uint32_t next_id = dc_get_next_media(dcn_context->dc_context,
msg_id,
dir,
msg_type1,
msg_type2,
msg_type3);
//TRACE("result %d", next_id);
NAPI_RETURN_UINT32(next_id);
}
NAPI_METHOD(dcn_set_chat_visibility) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
@@ -3422,6 +3444,7 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt);
NAPI_EXPORT_FUNCTION(dcn_get_msg_info);
NAPI_EXPORT_FUNCTION(dcn_get_msg_html);
NAPI_EXPORT_FUNCTION(dcn_get_next_media);
NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility);
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr);
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg);

View File

@@ -26,8 +26,6 @@ function createTempUser(chatmailDomain) {
}
describe('static tests', function () {
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
it('reverse lookup of events', function () {
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
const eventValues = Object.values(EventId2EventName)
@@ -271,7 +269,7 @@ describe('Basic offline Tests', function () {
'sync_msgs',
'sentbox_watch',
'show_emails',
'proxy_enabled',
'socks5_enabled',
'sqlite_version',
'uptime',
'used_account_settings',
@@ -703,7 +701,7 @@ describe('Offline Tests with unconfigured account', function () {
})
describe('Integration tests', function () {
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
this.timeout(60 * 3000) // increase timeout to 1min
let [dc, context, accountId, directory, account] = [
null,

View File

@@ -11,7 +11,7 @@
"chai": "~4.3.10",
"chai-as-promised": "^7.1.1",
"mocha": "^8.2.1",
"node-gyp": "~10.1.0",
"node-gyp": "^10.0.0",
"prebuildify": "^5.0.1",
"prebuildify-ci": "^1.0.5",
"prettier": "^3.0.3",
@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.149.0"
"version": "1.137.2"
}

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