mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
5 Commits
link2xt/ol
...
link2xt/sq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5820c4ce95 | ||
|
|
12ba33d9d4 | ||
|
|
60a7bbc9b5 | ||
|
|
e9f434b562 | ||
|
|
2423cb8175 |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -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"
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.80.1
|
||||
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.80.1
|
||||
rust: 1.77.1
|
||||
- os: windows-latest
|
||||
rust: 1.80.1
|
||||
rust: 1.77.1
|
||||
- os: macos-latest
|
||||
rust: 1.80.1
|
||||
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
|
||||
|
||||
138
.github/workflows/deltachat-rpc-server.yml
vendored
138
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -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
|
||||
|
||||
80
.github/workflows/jsonrpc-client-npm-package.yml
vendored
80
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -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/"
|
||||
|
||||
2
.github/workflows/node-docs.yml
vendored
2
.github/workflows/node-docs.yml
vendored
@@ -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 }}
|
||||
|
||||
45
.github/workflows/upload-docs.yml
vendored
45
.github/workflows/upload-docs.yml
vendored
@@ -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/"
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -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
|
||||
877
CHANGELOG.md
877
CHANGELOG.md
@@ -1,854 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.142.6] - 2024-08-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Default to strict TLS checks if not configured.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deltachat-rpc-client: Fix ruff 0.6.0 warnings.
|
||||
|
||||
## [1.142.5] - 2024-08-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
|
||||
- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
|
||||
- Increase timeout for QR generation to 60s ([#5882](https://github.com/deltachat/deltachat-core-rust/pull/5882)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document new `mdns_enabled` behavior (bots do not send MDNs by default).
|
||||
|
||||
### CI
|
||||
|
||||
- Configure Dependabot to update GitHub Actions.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump regex from 1.10.5 to 1.10.6.
|
||||
- cargo: Bump serde from 1.0.204 to 1.0.205.
|
||||
- deps: Bump horochx/deploy-via-scp from 1.0.1 to 1.1.0.
|
||||
- deps: Bump dependabot/fetch-metadata from 1.1.1 to 2.2.0.
|
||||
- deps: Bump actions/setup-node from 2 to 4.
|
||||
- Update provider database.
|
||||
|
||||
## [1.142.4] - 2024-08-09
|
||||
|
||||
### Build system
|
||||
|
||||
- Downgrade Tokio to 1.38 to fix Android compilation.
|
||||
- Use `--locked` with `cargo install`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add Config::FixIsChatmail.
|
||||
- Always move outgoing auto-generated messages to the mvbox.
|
||||
- Disable requesting MDNs for bots by default.
|
||||
- Allow using OAuth 2 with SOCKS5.
|
||||
- Allow autoconfig when SOCKS5 is enabled.
|
||||
- Update provider database.
|
||||
- cargo: Update iroh from 0.21 to 0.22 ([#5860](https://github.com/deltachat/deltachat-core-rust/pull/5860)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.80.1.
|
||||
- Update EmbarkStudios/cargo-deny-action.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Point to active Header Protection draft
|
||||
|
||||
### Refactor
|
||||
|
||||
- Derive `Default` for `CertificateChecks`.
|
||||
- Merge imap_certificate_checks and smtp_certificate_checks.
|
||||
- Remove param_addr_urlencoded argument from get_autoconfig().
|
||||
- Pass address to moz_autoconfigure() instead of LoginParam.
|
||||
|
||||
## [1.142.3] - 2024-08-04
|
||||
|
||||
### Build system
|
||||
|
||||
- cargo: Update rusqlite and libsqlite3-sys.
|
||||
- Fix cargo warnings about default-features
|
||||
- Do not disable "vendored" feature in the workspace.
|
||||
- cargo: Bump quick-xml from 0.35.0 to 0.36.1.
|
||||
- cargo: Bump uuid from 1.9.1 to 1.10.0.
|
||||
- cargo: Bump tokio from 1.38.0 to 1.39.2.
|
||||
- cargo: Bump env_logger from 0.11.3 to 0.11.5.
|
||||
- Remove sha2 dependency.
|
||||
- Remove `backtrace` dependency.
|
||||
- Remove direct "quinn" dependency.
|
||||
|
||||
## [1.142.2] - 2024-08-02
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Try only the full email address if username is unspecified.
|
||||
- Sort DNS results by successful connection timestamp ([#5818](https://github.com/deltachat/deltachat-core-rust/pull/5818)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Await the tasks after aborting them.
|
||||
- Do not reset is_chatmail config on failed reconfiguration.
|
||||
- Fix compilation on iOS.
|
||||
- Reset configured_provider on reconfiguration.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't update message state to `OutMdnRcvd` anymore.
|
||||
|
||||
### Build system
|
||||
|
||||
- Use workspace dependencies to make cargo-deny 0.15.1 happy.
|
||||
- cargo: Update bytemuck from 0.14.3 to 0.16.3.
|
||||
- cargo: Bump toml from 0.8.14 to 0.8.15.
|
||||
- cargo: Bump serde_json from 1.0.120 to 1.0.122.
|
||||
- cargo: Bump human-panic from 2.0.0 to 2.0.1.
|
||||
- cargo: Bump thiserror from 1.0.61 to 1.0.63.
|
||||
- cargo: Bump syn from 2.0.68 to 2.0.72.
|
||||
- cargo: Bump quoted_printable from 0.5.0 to 0.5.1.
|
||||
- cargo: Bump serde from 1.0.203 to 1.0.204.
|
||||
|
||||
## [1.142.1] - 2024-07-30
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not reveal sender's language in read receipts ([#5802](https://github.com/deltachat/deltachat-core-rust/pull/5802)).
|
||||
- Try next DNS resolution result if TLS setup fails.
|
||||
- Report first error instead of the last on connection failure.
|
||||
|
||||
### Fixes
|
||||
|
||||
- smtp: Use DNS cache for implicit TLS connections.
|
||||
- Imex::import_backup: Unpack all blobs before importing a db ([#4307](https://github.com/deltachat/deltachat-core-rust/pull/4307)).
|
||||
- Import_backup_stream: Fix progress stucking at 0.
|
||||
- Sql::import: Detach backup db if any step of the import fails.
|
||||
- Imex::import_backup: Ignore errors from delete_and_reset_all_device_msgs().
|
||||
- Explicitly close the database on account removal.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update time from 0.3.34 to 0.3.36.
|
||||
- cargo: Update iroh from 0.20.0 to 0.21.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Add net/dns submodule.
|
||||
- Pass single ALPN around instead of ALPN list.
|
||||
- Replace {IMAP,SMTP,HTTP}_TIMEOUT with a single constant.
|
||||
- smtp: Unify SMTP connection setup between TLS and STARTTLS.
|
||||
- imap: Unify IMAP connection setup in Client::connect().
|
||||
- Move DNS resolution into IMAP and SMTP connect code.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.80.0.
|
||||
|
||||
## [1.142.0] - 2024-07-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-jsonrpc: Add `pinned` property to `FullChat` and `BasicChat`.
|
||||
- deltachat-jsonrpc: Allow to set message quote text without referencing quoted message ([#5695](https://github.com/deltachat/deltachat-core-rust/pull/5695)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- cargo: Update iroh from 0.17 to 0.20.
|
||||
- iroh: Pass direct addresses from Endpoint to Gossip.
|
||||
- New BACKUP2 transfer protocol.
|
||||
- Use `[...]` instead of `...` for protected subject.
|
||||
- Add email address and fingerprint to exported key file names ([#5694](https://github.com/deltachat/deltachat-core-rust/pull/5694)).
|
||||
- Request `imap` ALPN for IMAP TLS connections and `smtp` ALPN for SMTP TLS connections.
|
||||
- Limit the size of aggregated WebXDC update to 100 KiB ([#4825](https://github.com/deltachat/deltachat-core-rust/pull/4825)).
|
||||
- Don't create ad-hoc group on a member removal message ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)).
|
||||
- Don't unarchive a group on a member removal except SELF ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)).
|
||||
- Use custom DNS resolver for HTTP(S).
|
||||
- Promote fallback DNS results to cached on successful use.
|
||||
- Set summary thumbnail path for WebXDCs to "webxdc-icon://last-msg-id" ([#5782](https://github.com/deltachat/deltachat-core-rust/pull/5782)).
|
||||
- Do not show the address in invite QR code SVG.
|
||||
- Report better error from DcKey::from_asc() ([#5539](https://github.com/deltachat/deltachat-core-rust/pull/5539)).
|
||||
- Contact::create_ex: Don't send sync message if nothing changed ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- `Message::set_quote`: Don't forget to remove `Param::ProtectQuote`.
|
||||
- Randomize avatar blob filenames to work around caching.
|
||||
- Correct copy-pasted DCACCOUNT parsing errors message.
|
||||
- Call `send_sync_msg()` only from the SMTP loop ([#5780](https://github.com/deltachat/deltachat-core-rust/pull/5780)).
|
||||
- Emit MsgsChanged if the number of unnoticed archived chats could decrease ([#5768](https://github.com/deltachat/deltachat-core-rust/pull/5768)).
|
||||
- Reject message with forged From even if no valid signatures are found.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move key transfer into its own submodule.
|
||||
- Move TempPathGuard into `tools` and use instead of `DeleteOnDrop`.
|
||||
- Return error from export_backup() without logging.
|
||||
- Reduce boilerplate for migration version increment.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add test for `get_http_response` JSON-RPC call.
|
||||
|
||||
### Build system
|
||||
|
||||
- node: Pin node-gyp to version 10.1.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update hashlink to remove allocator-api2 dependency.
|
||||
- cargo: Update openssl to v0.10.66.
|
||||
- deps: Bump openssl from 0.10.60 to 0.10.66 in /fuzz.
|
||||
- cargo: Update `image` crate to 0.25.2.
|
||||
|
||||
## [1.141.2] - 2024-07-09
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add `is_muted` config option.
|
||||
- Parse vcards exported by protonmail ([#5723](https://github.com/deltachat/deltachat-core-rust/pull/5723)).
|
||||
- Disable sending sync messages for bots ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't fail if going to send plaintext, but some peerstate is missing.
|
||||
- Correctly sanitize input everywhere ([#5697](https://github.com/deltachat/deltachat-core-rust/pull/5697)).
|
||||
- Do not try to register non-iOS tokens for heartbeats.
|
||||
- imap: Reset new_mail if folder is ignored.
|
||||
- Use and prefer Date from signed message part ([#5716](https://github.com/deltachat/deltachat-core-rust/pull/5716)).
|
||||
- Distinguish between database errors and no gossip topic.
|
||||
- MimeFactory::verified: Return true for self-chat.
|
||||
|
||||
### Refactor
|
||||
|
||||
- `MimeFactory::is_e2ee_guaranteed()`: always respect `Param::ForcePlaintext`.
|
||||
- Protect from reusing migration versions ([#5719](https://github.com/deltachat/deltachat-core-rust/pull/5719)).
|
||||
- Move `quota_needs_update` calculation to a separate function ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document vCards in the specification ([#5724](https://github.com/deltachat/deltachat-core-rust/pull/5724))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump toml from 0.8.13 to 0.8.14.
|
||||
- cargo: Bump serde_json from 1.0.117 to 1.0.120.
|
||||
- cargo: Bump syn from 2.0.66 to 2.0.68.
|
||||
- cargo: Bump async-broadcast from 0.7.0 to 0.7.1.
|
||||
- cargo: Bump url from 2.5.0 to 2.5.2.
|
||||
- cargo: Bump log from 0.4.21 to 0.4.22.
|
||||
- cargo: Bump regex from 1.10.4 to 1.10.5.
|
||||
- cargo: Bump proptest from 1.4.0 to 1.5.0.
|
||||
- cargo: Bump uuid from 1.8.0 to 1.9.1.
|
||||
- cargo: Bump backtrace from 0.3.72 to 0.3.73.
|
||||
- cargo: Bump quick-xml from 0.31.0 to 0.35.0.
|
||||
- cargo: Update yerpc to 0.6.2.
|
||||
- cargo: Update rPGP from 0.11 to 0.13.
|
||||
|
||||
## [1.141.1] - 2024-06-27
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update quota if it's stale, not fresh ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)).
|
||||
- sql: Assign migration adding msgs.deleted a new number.
|
||||
|
||||
### Refactor
|
||||
|
||||
- mimefactory: Factor out header confidentiality policy ([#5715](https://github.com/deltachat/deltachat-core-rust/pull/5715)).
|
||||
- Improve logging during SMTP/IMAP configuration.
|
||||
|
||||
## [1.141.0] - 2024-06-24
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-jsonrpc: Add `get_chat_securejoin_qr_code()`.
|
||||
- api!(deltachat-rpc-client): make {Account,Chat}.get_qr_code() return no SVG
|
||||
This is a breaking change, old method is renamed into `get_qr_code_svg()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Prefer references to fully downloaded messages for chat assignment ([#5645](https://github.com/deltachat/deltachat-core-rust/pull/5645)).
|
||||
- Protect From name for verified chats and To names for encrypted chats ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
- Display vCard contact name in the message summary.
|
||||
- Case-insensitive search for non-ASCII messages ([#5052](https://github.com/deltachat/deltachat-core-rust/pull/5052)).
|
||||
- Remove subject prefix from ad-hoc group names ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)).
|
||||
- Replace "Unnamed group" with "👥📧" to avoid translation.
|
||||
- Sync `Config::MvboxMove` across devices ([#5680](https://github.com/deltachat/deltachat-core-rust/pull/5680)).
|
||||
- Don't reveal profile data to a not yet verified contact ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
- Don't reveal profile data in MDNs ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fetch existing messages for bots as `InFresh` ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Keep tombstones for two days before deleting ([#3685](https://github.com/deltachat/deltachat-core-rust/pull/3685)).
|
||||
- Housekeeping: Delete MDNs and webxdc status updates for tombstones.
|
||||
- Delete user-deleted messages on the server even if they show up on IMAP later.
|
||||
- Do not send sync messages if bcc_self is disabled.
|
||||
- Don't generate Config sync messages for unconfigured accounts.
|
||||
- Do not require the Message to render MDN.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.79.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Remove outdated documentation comment from `send_smtp_messages`.
|
||||
- Remove misleading configuration comment.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update curve25519-dalek 4.1.x and suppress 3.2.0 warning.
|
||||
- Update provider database.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Deduplicate dependency versions ([#5691](https://github.com/deltachat/deltachat-core-rust/pull/5691)).
|
||||
- Store public key instead of secret key for peer channels.
|
||||
|
||||
### Tests
|
||||
|
||||
- Image drafted as Viewtype::File is sent as is.
|
||||
- python: Set delete_server_after=1 ("delete immediately") for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- deltachat-rpc-client: Test that webxdc realtime data is not reordered on the sender.
|
||||
- python: Wait for bot's DC_EVENT_IMAP_INBOX_IDLE before sending messages to it ([#5699](https://github.com/deltachat/deltachat-core-rust/pull/5699)).
|
||||
|
||||
## [1.140.2] - 2024-06-07
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add set_draft_vcard(.., msg_id, contacts).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Remove group member locally even if send_msg() fails ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
- Revert member addition if the corresponding message couldn't be sent ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
- @deltachat/stdio-rpc-server: Make local non-symlinked installation possible by using absolute paths for local dev version ([#5679](https://github.com/deltachat/deltachat-core-rust/pull/5679)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump schemars from 0.8.19 to 0.8.21.
|
||||
- cargo: Bump backtrace from 0.3.71 to 0.3.72.
|
||||
|
||||
### Refactor
|
||||
|
||||
- @deltachat/stdio-rpc-server: Use old school require instead of the experimental json import ([#5628](https://github.com/deltachat/deltachat-core-rust/pull/5628)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Set fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Don't leave protected group if some member's key is missing ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
|
||||
## [1.140.1] - 2024-06-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Retry sending MDNs on temporary error.
|
||||
- Set Config::IsChatmail in configure().
|
||||
- Do not miss new messages while expunging the folder.
|
||||
- Log messages with `info!` instead of `println!`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- imap: Document why CLOSE is faster than EXPUNGE.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: Make select_folder() accept non-optional folder.
|
||||
- Improve SMTP logs and errors.
|
||||
- Remove unused `select_folder::Error` variants.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: reenable `log_cli`.
|
||||
|
||||
## [1.140.0] - 2024-06-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
|
||||
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
|
||||
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
|
||||
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
|
||||
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
|
||||
- Allow creation of groups by outgoing messages without recipients.
|
||||
- Prefer `Chat-Group-ID` over references for new groups.
|
||||
- Do not fail to send images with wrong extensions.
|
||||
|
||||
### Build system
|
||||
|
||||
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Remove cargo-nextest bug workaround.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add vCard as supported standard.
|
||||
- Create_group() does not find chats, only creates them.
|
||||
- Fix a typo in test_partial_group_consistency().
|
||||
|
||||
### Refactor
|
||||
|
||||
- Factor create_adhoc_group() call out of create_group().
|
||||
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix logging of TestContext created using TestContext::new_alice().
|
||||
- Refactor `test_alias_*` into 8 separate tests.
|
||||
|
||||
## [1.139.6] - 2024-05-25
|
||||
|
||||
### Build system
|
||||
|
||||
- Update `iroh` to the git version.
|
||||
- nix: Add iroh-base output hash.
|
||||
- Upgrade iroh to 0.17.0.
|
||||
|
||||
### Fixes
|
||||
|
||||
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
|
||||
- Acquire write lock on iroh_channels before checking for subscribe_loop.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Fix python lint.
|
||||
- cargo-deny: Remove unused entry from deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Log IMAP connection type on connection failure.
|
||||
|
||||
### Tests
|
||||
|
||||
- Viewtype::File attachments are sent unchanged and preserve extensions.
|
||||
- deltachat-rpc-client: Add realtime channel tests.
|
||||
- deltachat-rpc-client: Regression test for double gossip subscription.
|
||||
|
||||
## [1.139.5] - 2024-05-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-ffi: Make WebXdcRealtimeData data usable in CFFI.
|
||||
- Add event channel overflow event.
|
||||
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_DATA constant.
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_advertisement().
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_data().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-repl: Add start-realtime and send-realtime commands.
|
||||
|
||||
### Fixes
|
||||
|
||||
- peer_channels: Connect to peers that advertise to you.
|
||||
- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)).
|
||||
|
||||
### Tests
|
||||
|
||||
- peer_channels: Add test_parallel_connect().
|
||||
- "SecureJoin wait" state and info messages.
|
||||
|
||||
## [1.139.4] - 2024-05-21
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Scale up contact origins to OutgoingTo when sending a message.
|
||||
- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not log warning if iroh relay metadata is NIL.
|
||||
- contact-tools: Parse_vcard: Support `\r\n` newlines.
|
||||
- Make_vcard: Add authname and key for ContactId::SELF.
|
||||
|
||||
### Other
|
||||
|
||||
- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)).
|
||||
|
||||
## [1.139.3] - 2024-05-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] @deltachat/stdio-rpc-server: change api: don't search in path unless `options.takeVersionFromPATH` is set to `true`
|
||||
- @deltachat/stdio-rpc-server: remove `DELTA_CHAT_SKIP_PATH` environment variable
|
||||
- @deltachat/stdio-rpc-server: remove version check / search for dc rpc server in $PATH
|
||||
- @deltachat/stdio-rpc-server: remove `options.skipSearchInPath`
|
||||
- @deltachat/stdio-rpc-server: add `options.takeVersionFromPATH`
|
||||
- deltachat-rpc-client: Add Account.wait_for_incoming_msg().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Replace env_logger with tracing_subscriber.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore event channel overflows.
|
||||
- mimeparser: Take the last header of multiple ones with the same name.
|
||||
- Db migration version 59, it contained an sql syntax error.
|
||||
- Sql syntax error in db migration 27.
|
||||
- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)).
|
||||
- @deltachat/stdio-rpc-server: set default options for `startDeltaChat`.
|
||||
- Always convert absolute paths to relative in accounts.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- receive_imf: Do not check for ContactId::UNDEFINED.
|
||||
- receive_imf: Remove unnecessary check for is_mdn.
|
||||
- receive_imf: Only call create_or_lookup_group() with allow_creation=true.
|
||||
- Use let..else in create_or_lookup_group().
|
||||
- Stop trying to extract chat ID from Message-IDs.
|
||||
- Do not try to lookup group in create_or_lookup_group().
|
||||
|
||||
## [1.139.2] - 2024-05-18
|
||||
|
||||
### Build system
|
||||
|
||||
- Add repository URL to @deltachat/jsonrpc-client.
|
||||
|
||||
## [1.139.1] - 2024-05-18
|
||||
|
||||
### CI
|
||||
|
||||
- Set `--access public` when publishing to npm.
|
||||
|
||||
## [1.139.0] - 2024-05-18
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Ephemeral peer channels ([#5346](https://github.com/deltachat/deltachat-core-rust/pull/5346)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Save override sender displayname for outgoing messages.
|
||||
- Do not mark the message as seen if it has `location.kml`.
|
||||
- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/deltachat/deltachat-core-rust/pull/5579)).
|
||||
- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/deltachat/deltachat-core-rust/pull/5583)).
|
||||
- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/deltachat/deltachat-core-rust/pull/5588))
|
||||
- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/deltachat/deltachat-core-rust/pull/5589))
|
||||
- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/deltachat/deltachat-core-rust/pull/5580))
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-jsonrpc: Return vcard contact directly in MessageObject.
|
||||
- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/deltachat/deltachat-core-rust/pull/5584)).
|
||||
- deltachat-rpc-client: Add ViewType.VCARD constant.
|
||||
- deltachat-rpc-client: Add Contact.make_vcard().
|
||||
- deltachat-rpc-client: Add Chat.send_contact().
|
||||
|
||||
### CI
|
||||
|
||||
- Publish @deltachat/jsonrpc-client directly to npm.
|
||||
- Check that constants are always up-to-date.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Add git-cliff to flake.
|
||||
- nix: Use rust-analyzer nightly
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Downgrade libc from 0.2.154 to 0.2.153.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: Test sending vCard.
|
||||
|
||||
## [1.138.5] - 2024-05-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add parse_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
|
||||
- Add Viewtype::Vcard ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
|
||||
- Add make_vcard() ([#5203](https://github.com/deltachat/deltachat-core-rust/pull/5203)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Add repository URL to deltachat-rpc-server packages.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Parsing vCards with avatars exported by Android's "Contacts" app.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Rebuild node constants.
|
||||
|
||||
### Refactor
|
||||
|
||||
- contact-tools: VcardContact: rename display_name to authname.
|
||||
- VcardContact: Change timestamp type to i64.
|
||||
|
||||
## [1.138.4] - 2024-05-15
|
||||
|
||||
### CI
|
||||
|
||||
- Run actions/setup-node before npm publish.
|
||||
|
||||
## [1.138.3] - 2024-05-15
|
||||
|
||||
### CI
|
||||
|
||||
- Give CI job permission to publish binaries to the release.
|
||||
|
||||
## [1.138.2] - 2024-05-15
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add CONFIG_SYNCED constant.
|
||||
|
||||
### CI
|
||||
|
||||
- Add npm token to publish deltachat-rpc-server packages.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Reset more settings when configuring a chatmail account.
|
||||
|
||||
### Tests
|
||||
|
||||
- Set configuration after configure() finishes.
|
||||
|
||||
## [1.138.1] - 2024-05-14
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Detect XCHATMAIL capability and expose it as `is_chatmail` config.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never treat message with Chat-Group-ID as a private reply.
|
||||
- Always prefer Chat-Group-ID over In-Reply-To and References.
|
||||
- Ignore parent message if message references itself.
|
||||
|
||||
### CI
|
||||
|
||||
- Set RUSTUP_WINDOWS_PATH_ADD_BIN to work around `nextest` issue <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
- deltachat-rpc-server: Fix upload of npm packages to github releases ([#5564](https://github.com/deltachat/deltachat-core-rust/pull/5564)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Add MimeMessage.get_chat_group_id().
|
||||
- Make MimeMessage.get_header() return Option<&str>.
|
||||
- sql: Make open flags immutable.
|
||||
- Resultify token::lookup_or_new().
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump parking_lot from 0.12.1 to 0.12.2.
|
||||
- cargo: Bump libc from 0.2.153 to 0.2.154.
|
||||
- cargo: Bump hickory-resolver from 0.24.0 to 0.24.1.
|
||||
- cargo: Bump serde_json from 1.0.115 to 1.0.116.
|
||||
- cargo: Bump human-panic from 1.2.3 to 2.0.0.
|
||||
- cargo: Bump brotli from 5.0.0 to 6.0.0.
|
||||
|
||||
## [1.138.0] - 2024-05-13
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add dc_msg_save_file() which saves file copy at the provided path ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
|
||||
- Api!(jsonrpc): replace EphemeralTimer tag "variant" with "kind"
|
||||
|
||||
### CI
|
||||
|
||||
- Use rsync instead of 3rd party github action.
|
||||
- Replace `black` with `ruff format`.
|
||||
- Update Rust to 1.78.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix references in Message.set_location() documentation.
|
||||
- Remove Doxygen markup from Message.has_location().
|
||||
- Add `location` module documentation.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Delete expired path locations in ephemeral loop.
|
||||
- Delete orphaned POI locations during housekeeping.
|
||||
- Parsing vCards for contacts sharing ([#5482](https://github.com/deltachat/deltachat-core-rust/pull/5482)).
|
||||
- contact-tools: Support parsing profile images from "PHOTO:data:image/jpeg;base64,...".
|
||||
- contact-tools: Add make_vcard().
|
||||
- Do not add location markers to messages with non-POI location.
|
||||
- Make one-to-one chats read-only the first seconds of a SecureJoin ([#5512](https://github.com/deltachat/deltachat-core-rust/pull/5512)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Message::set_file_from_bytes(): Set Param::Filename.
|
||||
- Do not fail to send encrypted quotes to unencrypted chats.
|
||||
- Never prepend subject to message text when bot receives it.
|
||||
- Interrupt location loop when new location is stored.
|
||||
- Correct message viewtype before recoding image blob ([#5496](https://github.com/deltachat/deltachat-core-rust/pull/5496)).
|
||||
- Delete POI location when disappearing message expires.
|
||||
- Delete non-POI locations after `delete_device_after`, not immediately.
|
||||
- Update special chats icons even if they are blocked ([#5509](https://github.com/deltachat/deltachat-core-rust/pull/5509)).
|
||||
- Use ChatIdBlocked::lookup_by_contact() instead of ChatId's method when applicable.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump quote from 1.0.35 to 1.0.36.
|
||||
- cargo: Bump base64 from 0.22.0 to 0.22.1.
|
||||
- cargo: Bump serde from 1.0.197 to 1.0.200.
|
||||
- cargo: Bump async-channel from 2.2.0 to 2.2.1.
|
||||
- cargo: Bump thiserror from 1.0.58 to 1.0.59.
|
||||
- cargo: Bump anyhow from 1.0.81 to 1.0.82.
|
||||
- cargo: Bump chrono from 0.4.37 to 0.4.38.
|
||||
- cargo: Bump imap-proto from 0.16.4 to 0.16.5.
|
||||
- cargo: Bump syn from 2.0.57 to 2.0.60.
|
||||
- cargo: Bump mailparse from 0.14.1 to 0.15.0.
|
||||
- cargo: Bump schemars from 0.8.16 to 0.8.19.
|
||||
|
||||
### Other
|
||||
|
||||
- Build ts docs with ci + nix.
|
||||
- Push docs to delta.chat instead of codespeak
|
||||
- Implement jsonrpc-docs build in github action
|
||||
- Rm unneeded rust install from ts docs ci
|
||||
- Correct folder for js.jsonrpc docs
|
||||
- Add npm install to upload-docs.yml
|
||||
- Add : to upload-docs.yml
|
||||
- Upload-docs npm run => npm run build
|
||||
- Rm leading slash
|
||||
- Rm npm install
|
||||
- Merge pull request #5515 from deltachat/dependabot/cargo/quote-1.0.36
|
||||
- Merge pull request #5522 from deltachat/dependabot/cargo/chrono-0.4.38
|
||||
- Merge pull request #5523 from deltachat/dependabot/cargo/mailparse-0.15.0
|
||||
- Add webxdc internal integration commands in jsonrpc ([#5541](https://github.com/deltachat/deltachat-core-rust/pull/5541))
|
||||
- Limit quote replies ([#5543](https://github.com/deltachat/deltachat-core-rust/pull/5543))
|
||||
- Stdio jsonrpc server npm package ([#5332](https://github.com/deltachat/deltachat-core-rust/pull/5332))
|
||||
|
||||
### Refactor
|
||||
|
||||
- python: Fix ruff 0.4.2 warnings.
|
||||
- Move `delete_poi_location` to location module and document it.
|
||||
- Remove allow_keychange.
|
||||
|
||||
### Tests
|
||||
|
||||
- Explain test_was_seen_recently false-positive and give workaround instructions ([#5474](https://github.com/deltachat/deltachat-core-rust/pull/5474)).
|
||||
- Test that member is added even if "Member added" is lost.
|
||||
- Test that POIs are deleted when ephemeral message expires.
|
||||
- Test ts build on branch
|
||||
|
||||
|
||||
## [1.137.4] - 2024-04-24
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `Stream` implementation for `EventEmitter`.
|
||||
- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/deltachat/deltachat-core-rust/pull/5461)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/deltachat/deltachat-core-rust/pull/5443)).
|
||||
- Replace event channel with broadcast channel.
|
||||
- Mark contact request messages as seen on IMAP.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Convert images to RGB8 (without alpha) before encoding into JPEG to fix sending of large RGBA images.
|
||||
- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/deltachat/deltachat-core-rust/pull/5445)).
|
||||
- Do not fail if Autocrypt Setup Message has no encryption preference to fix key transfer from K-9 Mail to Delta Chat.
|
||||
- Use only CRLF in Autocrypt Setup Message.
|
||||
- python: Use cached message object if `dc_get_msg()` returns `NULL`.
|
||||
- python: `Message::is_outgoing`: Don't reload message from db.
|
||||
- python: `_map_ffi_event`: Always check if `get_message_by_id()` returned None.
|
||||
- node: Undefine `NAPI_EXPERIMENTAL` to fix build with new clang.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Add `imap-tools` as `deltachat-rpc-client` dependency.
|
||||
- nix: Add `./deltachat-contact-tools` to sources.
|
||||
- nix: Update nix flake.
|
||||
- deps: Update rustls to 0.21.11.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update references to SecureJoin protocols.
|
||||
- Fix broken references in documentation comments.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: remove `RwLock` from `ratelimit`.
|
||||
- deltachat-ffi: Remove unused `ResultNullableExt`.
|
||||
- Remove duplicate clippy exceptions.
|
||||
- Group `use` at the top of the test modules.
|
||||
|
||||
## [1.137.3] - 2024-04-16
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove reactions ffi; all implementations use jsonrpc.
|
||||
- Don't load trashed messages with `Message::load_from_db`.
|
||||
- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/deltachat/deltachat-core-rust/pull/4476)).
|
||||
- deltachat-rpc-client: Add `check_qr` and `set_config_from_qr` APIs.
|
||||
- deltachat-rpc-client: Add `Account.create_chat()`.
|
||||
- deltachat-rpc-client: Add `Message.wait_until_delivered()`.
|
||||
- deltachat-rpc-client: Add `Chat.send_file()`.
|
||||
- deltachat-rpc-client: Add `Account.wait_for_reactions_changed()`.
|
||||
- deltachat-rpc-client: Return Message from `Message.send_reaction()`.
|
||||
- deltachat-rpc-client: Add `Account.bring_online()`.
|
||||
- deltachat-rpc-client: Add `ACFactory.get_accepted_chat()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Port `direct_imap.py` into deltachat-rpc-client.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not emit `MSGS_CHANGED` event for outgoing hidden messages.
|
||||
- `Message::get_summary()` must not return reaction summary.
|
||||
- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/deltachat/deltachat-core-rust/pull/5377)).
|
||||
- deltachat-jsonrpc: block in `inner_get_backup_qr`.
|
||||
- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/deltachat/deltachat-core-rust/pull/5366)).
|
||||
- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/deltachat/deltachat-core-rust/pull/5365)).
|
||||
- Don't try to do `fetch_move_delete()` if Trash is needed but not yet configured.
|
||||
- Assign messages to chats based on not fully downloaded references.
|
||||
- Do not create ad-hoc groups from partial downloads.
|
||||
- deltachat-rpc-client: construct Thread with `target` keyword argument.
|
||||
- Format error context in `Message::load_from_db`.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: adapt target install path if env var `CARGO_BUILD_TARGET` is set.
|
||||
- nix: Use stable Rust in flake.nix devshell.
|
||||
|
||||
### CI
|
||||
|
||||
- Use cargo-nextest instead of cargo-test.
|
||||
- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/deltachat/deltachat-core-rust/pull/5459)).
|
||||
- Typos in CI files ([#5453](https://github.com/deltachat/deltachat-core-rust/pull/5453)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add <https://deps.rs> badge.
|
||||
- Add 'Ubuntu Touch' to the list of 'frontend projects'
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not ignore `Contact::get_by_id` errors in `get_encrinfo`.
|
||||
- deltachat-rpc-client: Use `list`, `set` and `tuple` instead of `typing`.
|
||||
- Use `clone_from()` ([#5451](https://github.com/deltachat/deltachat-core-rust/pull/5451)).
|
||||
- Do not check for `is_trash()` in `get_last_reaction_if_newer_than()`.
|
||||
- Split off functional contact tools into its own crate ([#5444](https://github.com/deltachat/deltachat-core-rust/pull/5444))
|
||||
- Fix nightly clippy warnings.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test withdrawing group join QR codes.
|
||||
- `display_chat()`: Don't add day markers.
|
||||
- Move reaction tests to JSON-RPC.
|
||||
- node: Increase 'static tests' timeout to 5 minutes.
|
||||
|
||||
## [1.137.2] - 2024-04-05
|
||||
|
||||
### API-Changes
|
||||
@@ -4721,31 +3872,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
|
||||
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
|
||||
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
|
||||
[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3
|
||||
[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4
|
||||
[1.138.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.4...v1.138.0
|
||||
[1.138.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.0...v1.138.1
|
||||
[1.138.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.1...v1.138.2
|
||||
[1.138.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.2...v1.138.3
|
||||
[1.138.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.3...v1.138.4
|
||||
[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5
|
||||
[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0
|
||||
[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1
|
||||
[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2
|
||||
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
|
||||
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
|
||||
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
|
||||
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
|
||||
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0
|
||||
[1.140.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.0...v1.140.1
|
||||
[1.140.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.1...v1.140.2
|
||||
[1.141.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.2...v1.141.0
|
||||
[1.141.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.0...v1.141.1
|
||||
[1.141.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.1...v1.141.2
|
||||
[1.142.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.2...v1.142.0
|
||||
[1.142.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.0...v1.142.1
|
||||
[1.142.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.1...v1.142.2
|
||||
[1.142.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.2...v1.142.3
|
||||
[1.142.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.3...v1.142.4
|
||||
[1.142.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.4...v1.142.5
|
||||
[1.142.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.5...v1.142.6
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
2492
Cargo.lock
generated
2492
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
124
Cargo.toml
124
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.142.6"
|
||||
version = "1.137.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -34,83 +34,94 @@ 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 }
|
||||
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.12", default-features = false, features = ["deflate", "fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
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 = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
futures = "0.3"
|
||||
futures-lite = "2.3.0"
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
|
||||
iroh-net = { version = "0.22.0", default-features = false }
|
||||
iroh-gossip = { version = "0.22.0", default-features = false, features = ["net"] }
|
||||
kamadak-exif = "0.5.3"
|
||||
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 }
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.13", default-features = false }
|
||||
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.36"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
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"
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
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 }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
# 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]
|
||||
ansi_term = { workspace = true }
|
||||
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 }
|
||||
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]
|
||||
@@ -123,7 +134,6 @@ members = [
|
||||
"deltachat-repl",
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -154,41 +164,6 @@ harness = false
|
||||
name = "send_events"
|
||||
harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
ansi_term = "0.12.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" }
|
||||
deltachat = { path = "." }
|
||||
futures = "0.3.30"
|
||||
futures-lite = "2.3.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = "1.0"
|
||||
tempfile = "3.10.1"
|
||||
thiserror = "1"
|
||||
|
||||
# 1.38 is the latest version before `mio` dependency update
|
||||
# that broke compilation with Android NDK r23c and r24.
|
||||
# Version 1.39.0 cannot be compiled using these NDKs,
|
||||
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
|
||||
# for details.
|
||||
tokio = "~1.38.1"
|
||||
|
||||
tokio-util = "0.7.11"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
@@ -197,6 +172,3 @@ vendored = [
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
17
RELEASE.md
17
RELEASE.md
@@ -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 ''`.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -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.
|
||||
@@ -1,810 +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), warn(clippy::indexing_slicing))]
|
||||
#![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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.142.6"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -409,7 +406,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `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
|
||||
@@ -420,8 +417,7 @@ 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
|
||||
* 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,
|
||||
@@ -482,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().
|
||||
@@ -495,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.
|
||||
@@ -519,20 +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.
|
||||
* - `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 (default).
|
||||
* 1 = WebXDC realtime API is enabled.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -1131,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.
|
||||
*
|
||||
@@ -1193,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.
|
||||
*
|
||||
@@ -2505,7 +2462,6 @@ 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_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2552,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().
|
||||
*
|
||||
@@ -2621,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.
|
||||
@@ -2657,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.
|
||||
@@ -4118,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().
|
||||
@@ -4195,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.
|
||||
@@ -4403,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.
|
||||
@@ -5376,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
|
||||
*
|
||||
@@ -5492,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
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -5712,7 +5697,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
* Accept invalid certificates, including self-signed ones
|
||||
* or having incorrect hostname.
|
||||
*/
|
||||
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 2
|
||||
#define DC_CERTCK_ACCEPT_INVALID_CERTIFICATES 3
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6296,18 +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
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
@@ -6317,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
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6651,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.
|
||||
@@ -7369,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
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
)]
|
||||
@@ -31,6 +32,7 @@ use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::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
|
||||
@@ -562,11 +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::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +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::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -619,13 +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::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,
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,11 +656,8 @@ 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::EventChannelOverflow { .. } => 0,
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
@@ -681,7 +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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,10 +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::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()
|
||||
@@ -749,11 +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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,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,
|
||||
@@ -1076,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,
|
||||
@@ -2058,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();
|
||||
}
|
||||
}
|
||||
@@ -3381,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() {
|
||||
@@ -4284,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 _)
|
||||
@@ -4364,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();
|
||||
@@ -4418,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;
|
||||
}
|
||||
|
||||
@@ -4484,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
|
||||
@@ -4963,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);
|
||||
|
||||
@@ -50,7 +50,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
@@ -103,7 +102,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -129,7 +127,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -180,8 +177,6 @@ pub enum LotState {
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackup2 = 252,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.142.6"
|
||||
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"] }
|
||||
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 = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
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]
|
||||
|
||||
@@ -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()
|
||||
@@ -342,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())
|
||||
@@ -707,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.
|
||||
///
|
||||
@@ -734,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(
|
||||
@@ -744,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
|
||||
@@ -761,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()`.
|
||||
@@ -1459,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
|
||||
// ---------------------------------------------
|
||||
@@ -1648,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`].
|
||||
@@ -1670,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)
|
||||
}
|
||||
|
||||
@@ -1689,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
|
||||
}
|
||||
|
||||
@@ -1712,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?;
|
||||
@@ -1808,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,
|
||||
@@ -1880,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.
|
||||
@@ -2011,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
|
||||
@@ -2092,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(())
|
||||
}
|
||||
|
||||
@@ -2301,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),
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,10 +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> },
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
@@ -254,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 {
|
||||
@@ -369,19 +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::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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
@@ -176,13 +169,6 @@ impl MessageObject {
|
||||
Some(reactions.into())
|
||||
};
|
||||
|
||||
let vcard_contacts: Vec<VcardContact> = message
|
||||
.vcard_contacts(context)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(MessageObject {
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
@@ -242,8 +228,6 @@ impl MessageObject {
|
||||
download_state,
|
||||
|
||||
reactions,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -286,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 {
|
||||
@@ -307,7 +286,6 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +304,6 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,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,
|
||||
|
||||
@@ -391,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 {
|
||||
@@ -416,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,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 {
|
||||
@@ -615,9 +576,6 @@ impl MessageData {
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
} else if let Some(text) = self.quoted_text {
|
||||
let protect = false;
|
||||
message.set_quote_text(Some((text, protect)));
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
@@ -673,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,
|
||||
|
||||
@@ -35,11 +35,6 @@ pub enum QrObject {
|
||||
Backup {
|
||||
ticket: String,
|
||||
},
|
||||
Backup2 {
|
||||
auth_token: String,
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
@@ -137,14 +132,6 @@ impl From<Qr> for QrObject {
|
||||
Qr::Backup { ticket } => QrObject::Backup {
|
||||
ticket: ticket.to_string(),
|
||||
},
|
||||
Qr::Backup2 {
|
||||
ref node_addr,
|
||||
auth_token,
|
||||
} => QrObject::Backup2 {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
|
||||
auth_token,
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -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.142.6"
|
||||
"version": "1.137.2"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.142.6"
|
||||
version = "1.137.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = { workspace = true }
|
||||
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"]
|
||||
|
||||
@@ -19,7 +19,6 @@ 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::reaction::send_reaction;
|
||||
@@ -339,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\
|
||||
@@ -503,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");
|
||||
}
|
||||
@@ -631,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.");
|
||||
|
||||
@@ -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.
|
||||
//!
|
||||
@@ -32,7 +31,6 @@ use rustyline::{
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
@@ -152,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",
|
||||
@@ -163,6 +161,7 @@ const IMEX_COMMANDS: [&str; 13] = [
|
||||
"receive-backup",
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"export-setup",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
@@ -483,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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""
|
||||
Example echo bot without using hooks
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.142.6"
|
||||
version = "1.137.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -21,9 +21,6 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
@@ -37,7 +34,7 @@ deltachat_rpc_client = [
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [
|
||||
select = [
|
||||
"E", "W", # pycodestyle
|
||||
"F", # Pyflakes
|
||||
"N", # pep8-naming
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -59,10 +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"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
@@ -115,7 +111,6 @@ class ViewType(str, Enum):
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
|
||||
class SystemMessageType(str, Enum):
|
||||
@@ -165,7 +160,7 @@ class CertificateChecks(IntEnum):
|
||||
|
||||
AUTOMATIC = 0
|
||||
STRICT = 1
|
||||
ACCEPT_INVALID_CERTIFICATES = 2
|
||||
ACCEPT_INVALID_CERTIFICATES = 3
|
||||
|
||||
|
||||
class Connectivity(IntEnum):
|
||||
|
||||
@@ -60,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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,226 +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 imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
|
||||
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):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_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()
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -1,210 +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 sys
|
||||
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):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
|
||||
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")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == list(b"foo")
|
||||
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")
|
||||
@@ -1,14 +1,13 @@
|
||||
import logging
|
||||
|
||||
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()
|
||||
@@ -31,35 +30,13 @@ 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()
|
||||
|
||||
# Test that email address is in SVG
|
||||
# when we have no display name.
|
||||
# Check only the domain name, because
|
||||
# long address may be split over multiple lines
|
||||
# and not matched.
|
||||
assert domain in 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):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
@@ -69,7 +46,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
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)
|
||||
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
@@ -114,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()
|
||||
@@ -129,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)
|
||||
@@ -191,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()
|
||||
|
||||
@@ -211,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()
|
||||
|
||||
@@ -228,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()
|
||||
|
||||
@@ -275,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()
|
||||
|
||||
@@ -292,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()
|
||||
|
||||
@@ -359,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()
|
||||
|
||||
@@ -369,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()
|
||||
|
||||
@@ -382,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")
|
||||
@@ -404,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:
|
||||
@@ -425,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()
|
||||
@@ -448,7 +425,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
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()
|
||||
@@ -487,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()
|
||||
|
||||
@@ -543,16 +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()
|
||||
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()
|
||||
|
||||
@@ -612,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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -71,18 +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_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -118,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()
|
||||
@@ -459,179 +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())
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.142.6"
|
||||
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"]
|
||||
|
||||
3
deltachat-rpc-server/npm-package/.gitignore
vendored
3
deltachat-rpc-server/npm-package/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
platform_package
|
||||
*.tgz
|
||||
package-lock.json
|
||||
@@ -1,3 +0,0 @@
|
||||
platform_package/*
|
||||
scripts/
|
||||
*.tgz
|
||||
@@ -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.
|
||||
42
deltachat-rpc-server/npm-package/index.d.ts
vendored
42
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.142.6"
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
@@ -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));
|
||||
@@ -1,5 +0,0 @@
|
||||
//@ts-check
|
||||
|
||||
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
|
||||
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
@@ -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\``;
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
40
deny.toml
40
deny.toml
@@ -15,10 +15,6 @@ ignore = [
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
|
||||
# Problem in curve25519-dalek 3.2.0 used by iroh 0.4.
|
||||
# curve25519-dalek 4.1.3 has the problem fixed.
|
||||
"RUSTSEC-2024-0344",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -27,13 +23,9 @@ ignore = [
|
||||
# when upgrading.
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "asn1-rs-derive", version = "0.4.0" },
|
||||
{ name = "asn1-rs-impl", version = "0.1.0" },
|
||||
{ name = "asn1-rs", version = "0.5.2" },
|
||||
{ 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" },
|
||||
@@ -41,77 +33,45 @@ skip = [
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
{ name = "darling_macro", version = "<0.14" },
|
||||
{ name = "darling", version = "<0.14" },
|
||||
{ name = "der_derive", version = "0.6.1" },
|
||||
{ name = "derive_more", version = "0.99.17" },
|
||||
{ name = "der-parser", version = "8.2.0" },
|
||||
{ name = "der", version = "0.6.1" },
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "dlopen2", version = "0.4.1" },
|
||||
{ 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 = "futures-lite", version = "1.13.0" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "h2", version = "0.3.26" },
|
||||
{ name = "http-body", version = "0.4.6" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hyper", version = "0.14.28" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "netlink-packet-core", version = "0.5.0" },
|
||||
{ name = "netlink-packet-route", version = "0.15.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "oid-registry", version = "0.6.1" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pem", version = "1.1.1" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "rcgen", version = "<0.12.1" },
|
||||
{ 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 = "rustls-pemfile", version = "1.0.4" },
|
||||
{ name = "rustls", version = "0.21.11" },
|
||||
{ name = "rustls-webpki", version = "0.101.7" },
|
||||
{ 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 = "ssh-encoding", version = "0.1.0" },
|
||||
{ name = "ssh-key", version = "0.5.1" },
|
||||
{ name = "strsim", version = "0.10.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "synstructure", version = "0.12.6" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "system-configuration-sys", version = "0.5.0" },
|
||||
{ name = "system-configuration", version = "0.5.1" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-rustls", version = "0.24.1" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "webpki-roots", version ="0.25.4" },
|
||||
{ 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.52" },
|
||||
{ name = "windows-targets", version = "<0.52" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows", version = "<0.54.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" },
|
||||
{ name = "x509-parser", version = "<0.16.0" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
65
flake.lock
generated
65
flake.lock
generated
@@ -7,11 +7,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712088936,
|
||||
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
|
||||
"lastModified": 1710633978,
|
||||
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
|
||||
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -29,11 +29,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711099426,
|
||||
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
|
||||
"lastModified": 1708939976,
|
||||
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
|
||||
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -48,11 +48,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714112748,
|
||||
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
|
||||
"lastModified": 1710742993,
|
||||
"narHash": "sha256-W0PQCe0bW3hKF5lHawXrKynBcdSP18Qa4sb8DcUfOqI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
|
||||
"rev": "6f2fec850f569d61562d3a47dc263f19e9c7d825",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -84,11 +84,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -120,11 +120,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713520724,
|
||||
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
|
||||
"lastModified": 1698420672,
|
||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
|
||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -150,11 +150,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711703276,
|
||||
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -166,11 +166,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713895582,
|
||||
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
|
||||
"lastModified": 1710631334,
|
||||
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
|
||||
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -182,11 +182,12 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1711668574,
|
||||
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
|
||||
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
|
||||
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
|
||||
"type": "path"
|
||||
"lastModified": 1710765496,
|
||||
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
@@ -195,11 +196,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1714076141,
|
||||
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
|
||||
"lastModified": 1710631334,
|
||||
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
|
||||
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -222,11 +223,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1714031783,
|
||||
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
|
||||
"lastModified": 1710708100,
|
||||
"narHash": "sha256-Jd6pmXlwKk5uYcjyO/8BfbUVmx8g31Qfk7auI2IG66A=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
|
||||
"rev": "b6d1887bc4f9543b6c6bf098179a62446f34a6c3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
16
flake.nix
16
flake.nix
@@ -35,7 +35,6 @@
|
||||
./CMakeLists.txt
|
||||
./CONTRIBUTING.md
|
||||
./deltachat_derive
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-ratelimit
|
||||
@@ -483,7 +482,6 @@
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
};
|
||||
|
||||
@@ -525,15 +523,9 @@
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
(fenixPkgs.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
@@ -541,10 +533,8 @@
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
fenixPkgs.rust-analyzer
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
2695
fuzz/Cargo.lock
generated
2695
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 2,
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
|
||||
DC_CERTCK_AUTO: 0,
|
||||
DC_CERTCK_STRICT: 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT: 7,
|
||||
@@ -30,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,
|
||||
@@ -67,7 +64,6 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
@@ -112,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,
|
||||
@@ -128,7 +123,6 @@ 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,
|
||||
@@ -177,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,
|
||||
@@ -271,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,
|
||||
|
||||
@@ -37,9 +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',
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 2,
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||
@@ -30,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,
|
||||
@@ -67,7 +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_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
@@ -112,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,
|
||||
@@ -128,7 +123,6 @@ 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,
|
||||
@@ -177,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,
|
||||
@@ -271,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,
|
||||
@@ -341,9 +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',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#define NAPI_VERSION 4
|
||||
#define NAPI_EXPERIMENTAL
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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.142.6"
|
||||
"version": "1.137.2"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_configure_completed*
|
||||
""",
|
||||
)
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.142.6"
|
||||
version = "1.137.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
@@ -46,7 +46,7 @@ deltachat = [
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -328,12 +328,10 @@ class EventThread(threading.Thread):
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
if msg is not None:
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", {"chat": chat}
|
||||
|
||||
@@ -364,9 +364,6 @@ class Message:
|
||||
else:
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_state(dc_msg)
|
||||
|
||||
def is_in_fresh(self):
|
||||
@@ -395,7 +392,7 @@ class Message:
|
||||
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
return lib.dc_msg_get_state(self._dc_msg) in (
|
||||
return self._msgstate in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
@@ -487,9 +484,6 @@ class Message:
|
||||
|
||||
# load message from db to get a fresh/current state
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
# Message could be trashed, use the cached object if so.
|
||||
if dc_msg == ffi.NULL:
|
||||
dc_msg = self._dc_msg
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
def download_full(self) -> None:
|
||||
|
||||
@@ -275,7 +275,6 @@ class ACSetup:
|
||||
def __init__(self, testprocess, init_time) -> None:
|
||||
self._configured_events = Queue()
|
||||
self._account2state: Dict[Account, str] = {}
|
||||
self._account2config: Dict[Account, Dict[str, str]] = {}
|
||||
self._imap_cleaned: Set[str] = set()
|
||||
self.testprocess = testprocess
|
||||
self.init_time = init_time
|
||||
@@ -337,8 +336,6 @@ class ACSetup:
|
||||
if not success:
|
||||
pytest.fail(f"configuring online account {acc} failed: {comment}")
|
||||
self._account2state[acc] = self.CONFIGURED
|
||||
if acc in self._account2config:
|
||||
acc.update_config(self._account2config[acc])
|
||||
return acc
|
||||
|
||||
def _onconfigure_start_io(self, acc):
|
||||
@@ -526,7 +523,6 @@ class ACFactory:
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
ac.update_config(configdict)
|
||||
self._acsetup._account2config[ac] = configdict
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
return ac
|
||||
|
||||
@@ -552,15 +548,6 @@ class ACFactory:
|
||||
|
||||
bot_cfg = self.get_next_liveconfig()
|
||||
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
|
||||
self._acsetup.start_configure(bot_ac)
|
||||
self.wait_configured(bot_ac)
|
||||
bot_ac.start_io()
|
||||
# Wait for DC_EVENT_IMAP_INBOX_IDLE so that all emails appeared in the bot's Inbox later are
|
||||
# considered new and not existing ones, and thus processed by the bot.
|
||||
print(bot_ac._logid, "waiting for inbox IDLE to become ready")
|
||||
bot_ac._evtracker.wait_idle_inbox_ready()
|
||||
bot_ac.stop_io()
|
||||
self._acsetup._account2state[bot_ac] = self._acsetup.IDLEREADY
|
||||
|
||||
# Forget ac as it will be opened by the bot subprocess
|
||||
# but keep something in the list to not confuse account generation
|
||||
|
||||
@@ -18,14 +18,14 @@ def test_db_busy_error(acfactory):
|
||||
|
||||
# make a number of accounts
|
||||
accounts = acfactory.get_many_online_accounts(3)
|
||||
log(f"created {len(accounts)} accounts")
|
||||
log("created %s accounts" % len(accounts))
|
||||
|
||||
# put a bigfile into each account
|
||||
for acc in accounts:
|
||||
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
|
||||
with open(acc.bigfile, "wb") as f:
|
||||
f.write(b"01234567890" * 1000_000)
|
||||
log(f"created {len(accounts)} bigfiles")
|
||||
log("created %s bigfiles" % len(accounts))
|
||||
|
||||
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
|
||||
chat = accounts[0].create_group_chat("stress-group")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
@@ -676,17 +675,3 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
msg = chat.send_text("hello")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
ac1.delete_messages([msg])
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELETED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
time.sleep(5)
|
||||
assert len(chat.get_messages()) == 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -484,16 +485,6 @@ def test_move_works_on_self_sent(acfactory):
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
|
||||
def test_move_sync_msgs(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -1353,6 +1344,7 @@ def test_quote_encrypted(acfactory, lp):
|
||||
|
||||
for quoted_msg in msg1, msg3:
|
||||
# Save the draft with a quote.
|
||||
# It should be encrypted if quoted message is encrypted.
|
||||
msg_draft = Message.new_empty(ac1, "text")
|
||||
msg_draft.set_text("message reply")
|
||||
msg_draft.quote = quoted_msg
|
||||
@@ -1366,14 +1358,10 @@ def test_quote_encrypted(acfactory, lp):
|
||||
chat.set_draft(None)
|
||||
assert chat.get_draft() is None
|
||||
|
||||
# Quote should be replaced with "..." if quoted message is encrypted.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message reply"
|
||||
assert not msg_in.is_encrypted()
|
||||
if quoted_msg.is_encrypted():
|
||||
assert msg_in.quoted_text == "..."
|
||||
else:
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
|
||||
|
||||
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
@@ -1505,6 +1493,107 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
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()
|
||||
|
||||
reactions_queue = queue.Queue()
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_reactions_changed(self, message):
|
||||
reactions_queue.put(message)
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("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:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
|
||||
lp.sec("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))
|
||||
ac1._evtracker.wait_msg_delivered(msgs[-1])
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1_addr
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
"""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_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
lp.sec("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# 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}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
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")
|
||||
|
||||
lp.sec("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.text == msg1.text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1.get_config("addr")
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1572,6 +1661,8 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
# check progress events for import
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
|
||||
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
|
||||
assert imex_tracker.wait_progress(1000)
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
@@ -2044,15 +2135,14 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert chat1.is_sending_locations()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
# Wait for "enabled location streaming" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
# First location is sent immediately as a location-only message.
|
||||
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
|
||||
ac1._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
chat1.send_text("🍞")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
lp.sec("ac2: wait for incoming location message")
|
||||
|
||||
# currently core emits location changed before event_incoming message
|
||||
ac2._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
|
||||
locations = chat2.get_locations()
|
||||
@@ -2061,7 +2151,7 @@ def test_send_receive_locations(acfactory, lp):
|
||||
assert locations[0].longitude == 3.0
|
||||
assert locations[0].accuracy == 0.5
|
||||
assert locations[0].timestamp > now
|
||||
assert locations[0].marker is None
|
||||
assert locations[0].marker == "🍞"
|
||||
|
||||
contact = ac2.create_contact(ac1)
|
||||
locations2 = chat2.get_locations(contact=contact)
|
||||
@@ -2140,21 +2230,17 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
|
||||
def test_trash_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
# Create the Trash folder on IMAP server
|
||||
# and recreate the account so Trash folder is configured.
|
||||
lp.sec("Creating trash folder")
|
||||
ac2.direct_imap.create_folder("Trash")
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
|
||||
lp.sec("Check that Trash can be configured initially as well")
|
||||
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
lp.sec("Creating new accounts")
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
assert ac2.get_config("configured_trash_folder")
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending 3 messages")
|
||||
@@ -2169,9 +2255,6 @@ def test_trash_multiple_messages(acfactory, lp):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
lp.sec("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
|
||||
@@ -41,12 +41,13 @@ skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
black
|
||||
# pygments required by rst-lint
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
black --quiet --check --diff setup.py src/deltachat examples/ tests/
|
||||
ruff src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
@@ -61,8 +62,7 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
# Pinned version, workaround for <https://github.com/breathe-doc/breathe/issues/981>
|
||||
sphinx<7.3
|
||||
sphinx
|
||||
breathe
|
||||
sphinx_rtd_theme
|
||||
commands =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-08-15
|
||||
2024-04-05
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.80.1
|
||||
RUST_VERSION=1.77.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user