mirror of
https://github.com/chatmail/core.git
synced 2026-09-21 20:51:23 +03:00
Compare commits
1 Commits
link2xt/re
...
link2xt/pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a128cf04a |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.98.1
|
||||
RUST_VERSION: 1.97.1
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
@@ -126,13 +126,13 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
|
||||
uses: taiki-e/install-action@3d7d7cd5ac7f994c1892ae0c06165095b9139094
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
|
||||
10
.github/workflows/deltachat-rpc-server.yml
vendored
10
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -370,14 +370,6 @@ jobs:
|
||||
- name: List artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
- name: Check that the wheel metadata reads back
|
||||
run: |
|
||||
echo 'You can check a local `nix build` on non-Mac machines against the following checksums.'
|
||||
sha256sum dist/*.whl
|
||||
mkdir tagcheck
|
||||
cp dist/*.whl tagcheck/
|
||||
nix run --inputs-from . nixpkgs#python3Packages.wheel -- tags tagcheck/*.whl
|
||||
|
||||
- name: Upload binaries to the GitHub release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
@@ -390,7 +382,7 @@ jobs:
|
||||
|
||||
- name: Publish deltachat-rpc-server to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
|
||||
publish_npm_package:
|
||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||
|
||||
@@ -31,15 +31,15 @@ jobs:
|
||||
package-manager-cache: false # never use caching in release builds
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Package
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: |
|
||||
npm run build
|
||||
npm pack .
|
||||
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
|
||||
10
.github/workflows/jsonrpc.yml
vendored
10
.github/workflows/jsonrpc.yml
vendored
@@ -25,21 +25,21 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Add Rust cache
|
||||
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-bin: false
|
||||
- name: npm install
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run build
|
||||
- name: Run integration tests
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
- name: Run linter
|
||||
working-directory: deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@@ -5,13 +5,11 @@ on:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- nix/**
|
||||
- .github/workflows/nix.yml
|
||||
push:
|
||||
paths:
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- nix/**
|
||||
- .github/workflows/nix.yml
|
||||
branches:
|
||||
- main
|
||||
@@ -28,7 +26,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- run: nix fmt flake.nix nix/ -- --check
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
name: nix build
|
||||
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish deltachat-rpc-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./deltachat-jsonrpc-bindings/typescript
|
||||
working-directory: ./deltachat-jsonrpc/typescript
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
||||
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:"
|
||||
|
||||
build-cffi:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||
|
||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -1,112 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [2.60.0] - 2026-09-11
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] remove a relay immediately instead of unpublishing it.
|
||||
- `set_transport_unpublished()` is removed: UIs call `delete_transport()` when the user removes a relay.
|
||||
- `list_transports_ex()` and the `TransportListEntry` type are removed: use `list_transports()`.
|
||||
- `delete_transport()` no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed.
|
||||
- `TransportsModified` is now also emitted on the device modifying the transports, not only on devices applying the synced change.
|
||||
- [**breaking**] do not load webxdc icon if it has too large dimensions.
|
||||
- `get_webxdc_blob()` may fail to load `icon.png` or `icon.jpg` if image dimensions are too large.
|
||||
Fixing the issue discovered by https://github.com/Sergei768
|
||||
- Generate JSON-RPC headers at build time ([#8350](https://github.com/chatmail/core/pull/8350)).
|
||||
- Generate Qt JSON-RPC bindings ([#8330](https://github.com/chatmail/core/pull/8330)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Introduce keyupdate messages informing contacts about relay changes.
|
||||
- Remove `Final-Recipient` from MDNs (and keyupdates).
|
||||
- Carry all published relay addresses in securejoin links ([#8591](https://github.com/chatmail/core/pull/8591)).
|
||||
- Use display name for contacts in encryption info ([#8609](https://github.com/chatmail/core/pull/8609)).
|
||||
- Do not create device messages for IMAP authentication errors.
|
||||
- Delete avatars referred to by parameters of special contacts.
|
||||
- Import `Autocrypt-Gossip` keys without checking the addresses.
|
||||
- Ignore `Chat-Disposition-Notification-To` value.
|
||||
- Increase `sys.msgsize_max_recommended` to match chatmail relay message size limit.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not try to load profile image from param for self.
|
||||
- Send legacy securejoin key requests as `multipart/mixed` so they are not rejected by chatmail relays.
|
||||
- rpc: avoid hang when requests race a dying rpc-server.
|
||||
- Take `timestamp_rcvd` into account in `estimate_deletion_cnt`.
|
||||
- Reliably complete configuration with progress=1000 or progress=0.
|
||||
- Make `create_send_msg_jobs` actually return row IDs.
|
||||
- Don't notify of missed call from blocked user.
|
||||
- Trash MDNs that reference no message early.
|
||||
- Return no relay address for key-contacts without an address.
|
||||
- Do not emit events in `set_profile_image()` if contact avatar is unchanged.
|
||||
- Remove `Original-Recipient` field from MDNs.
|
||||
- ffi: support custom allocators in event string getters.
|
||||
- Start checking column documentation in CI and add comment for `transports.add_timestamp`.
|
||||
- Sanitize `version_string` we got from the wire ([#8582](https://github.com/chatmail/core/pull/8582))
|
||||
- RUSTSEC-2026-0258 ([#8603](https://github.com/chatmail/core/pull/8603)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Use `--locked` in `scripts/clippy.sh`.
|
||||
- Produce correct wheel metadata.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Always suggest using `--locked` with "cargo install".
|
||||
- JSON-RPC: clarify when `reactions` is `None`.
|
||||
- Fix async-imap and async-smtp URLs in README.md ([#8637](https://github.com/chatmail/core/pull/8637)).
|
||||
- Update the timeout value in `DC_EVENT_CALL_ENDED` description.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't store email address in location KML. ([#8615](https://github.com/chatmail/core/pull/8615)).
|
||||
- Turn `DC_CHAT_ID_*` into `ChatId::*` associated constants.
|
||||
- Turn `DC_MSG_ID_*` into `MsgId::*` associated constants.
|
||||
- Don't include email addresses in export filenames ([#8626](https://github.com/chatmail/core/pull/8626)).
|
||||
- Make `create_send_msg_jobs()` private.
|
||||
- Rename `automatic_relay_management` to autorelay.
|
||||
- Extract shared pieces for non-chat messages.
|
||||
- Remove unused functions from the tools module.
|
||||
- Remove the code to set own avatar in `set_profile_image()`.
|
||||
- Move pgp tests to submodule.
|
||||
- Split `flake.nix` into multiple files.
|
||||
- Use `&[..]` instead of `&Vec<..>`.
|
||||
|
||||
### Tests
|
||||
|
||||
- [**breaking**] rename rpc fixtures to disambiguate from ffi fixtures.
|
||||
- Test `dc_send_msg_sync()`.
|
||||
- Print which error/warning was expected if it does not arrive.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.98.1.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add script to show the sizes of futures (async Rust) ([#8536](https://github.com/chatmail/core/pull/8536)).
|
||||
- deps: bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2.
|
||||
- deps: bump swatinem/rust-cache from 2.9.1 to 2.9.2.
|
||||
- deps: bump pypa/gh-action-pypi-publish from 1.14.1 to 1.14.2.
|
||||
- deps: bump taiki-e/install-action from 2.85.1 to 2.86.7.
|
||||
- cargo: bump futures from 0.3.33 to 0.3.34.
|
||||
- cargo: bump thiserror from 2.0.19 to 2.0.20.
|
||||
- cargo: bump syn from 3.0.3 to 3.0.4.
|
||||
- cargo: bump log from 0.4.33 to 0.4.34.
|
||||
- cargo: bump mail-builder from 0.4.4 to 0.5.0.
|
||||
- cargo: bump blake3 from 1.8.5 to 1.8.7.
|
||||
- cargo: bump http-body-util from 0.1.3 to 0.1.5.
|
||||
- cargo: bump uuid from 1.20.0 to 1.25.0.
|
||||
- cargo: bump data-encoding from 2.11.0 to 2.11.1.
|
||||
- bump chacha20 0.10.1 to 0.10.2.
|
||||
|
||||
## [2.59.0] - 2026-08-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
|
||||
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
|
||||
- New `get_app_version()` JSON-RPC API to get information about available updates.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
@@ -8793,4 +8692,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.57.0]: https://github.com/chatmail/core/compare/v2.56.0..v2.57.0
|
||||
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
|
||||
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
|
||||
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
|
||||
|
||||
@@ -2,59 +2,45 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat LANGUAGES C)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
if(APPLE)
|
||||
set(DYNAMIC_EXT "dylib")
|
||||
set(DYNAMIC_EXT "dylib")
|
||||
elseif(UNIX)
|
||||
set(DYNAMIC_EXT "so")
|
||||
set(DYNAMIC_EXT "so")
|
||||
else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release")
|
||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
else()
|
||||
set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release")
|
||||
set(ARCH_DIR "./")
|
||||
endif()
|
||||
|
||||
if(WITH_JSONRPC_BINDINGS)
|
||||
set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env
|
||||
CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target"
|
||||
PREFIX="${CMAKE_INSTALL_PREFIX}"
|
||||
LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}"
|
||||
INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}"
|
||||
${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS}
|
||||
WORKING_DIRECTORY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
|
||||
add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
if(WITH_JSONRPC_BINDINGS)
|
||||
install(
|
||||
FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/types.hpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/generated/client.hpp"
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated"
|
||||
)
|
||||
install(
|
||||
FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp"
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc"
|
||||
)
|
||||
endif()
|
||||
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)
|
||||
|
||||
174
Cargo.lock
generated
174
Cargo.lock
generated
@@ -310,7 +310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -327,7 +327,7 @@ dependencies = [
|
||||
"log",
|
||||
"nom 8.0.0",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -363,7 +363,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -459,7 +459,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -497,10 +497,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.7"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
|
||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
@@ -804,9 +805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
@@ -1312,9 +1313,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.1"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "dbl"
|
||||
@@ -1327,7 +1328,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1399,7 +1400,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"testdir",
|
||||
"textwrap",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
@@ -1435,7 +1436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1454,16 +1455,9 @@ dependencies = [
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1479,7 +1473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1503,12 +1497,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1518,7 +1512,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"yerpc",
|
||||
]
|
||||
@@ -2127,9 +2121,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2155,9 +2149,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2180,15 +2174,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -2197,9 +2191,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2216,32 +2210,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2376,9 +2370,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.16"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -2465,7 +2459,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2488,7 +2482,7 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2587,9 +2581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -3045,7 +3039,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"surge-ping",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -3070,7 +3064,7 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -3112,7 +3106,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serde-error",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -3157,7 +3151,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3177,7 +3171,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -3232,7 +3226,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"strum 0.26.2",
|
||||
"stun-rs",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
@@ -3398,9 +3392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -3444,9 +3438,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
|
||||
|
||||
[[package]]
|
||||
name = "mail-builder"
|
||||
version = "0.5.0"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
|
||||
checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b"
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
@@ -3717,7 +3711,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4206,7 +4200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -4382,7 +4376,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
"simple-dns",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -4780,7 +4774,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -4801,7 +4795,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -4901,7 +4895,7 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20 0.10.2",
|
||||
"chacha20 0.10.1",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
@@ -5040,7 +5034,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5282,9 +5276,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"brotli-decompressor",
|
||||
@@ -5292,7 +5286,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.15",
|
||||
"rustls-webpki 0.103.13",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5329,9 +5323,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -5561,7 +5555,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5717,7 +5711,7 @@ dependencies = [
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.9",
|
||||
"spin 0.10.1",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
"trait-variant",
|
||||
@@ -6069,9 +6063,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.4"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6215,11 +6209,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.20",
|
||||
"thiserror-impl 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6235,13 +6229,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.4",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6763,11 +6757,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.25.0"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -7429,7 +7423,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"thiserror 2.0.19",
|
||||
"windows 0.59.0",
|
||||
"windows-core 0.59.0",
|
||||
]
|
||||
@@ -7535,9 +7529,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc"
|
||||
version = "0.7.0"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6924db1f1011f1d22566c45c7090aa66c3107c3c463ca32beaa8b2327127040a"
|
||||
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 1.9.0",
|
||||
@@ -7555,9 +7549,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc_derive"
|
||||
version = "0.7.0"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42c374248c189b15a7f0660db3984e9dcc790ec15f4b44e932e92be49de829f9"
|
||||
checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"darling",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -70,7 +70,7 @@ iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.5.0", default-features = false }
|
||||
mail-builder = { version = "0.4.4", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.17"
|
||||
@@ -129,7 +129,6 @@ members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc",
|
||||
"deltachat-jsonrpc-bindings",
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-ratelimit",
|
||||
"deltachat-repl",
|
||||
@@ -203,7 +202,7 @@ thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.7"
|
||||
yerpc = "0.6.4"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -21,8 +21,8 @@ The following protocols are handled without requiring API users to know much abo
|
||||
|
||||
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
||||
|
||||
- robust [SMTP](https://github.com/chatmail/async-smtp)
|
||||
and [IMAP](https://github.com/chatmail/async-imap) handling
|
||||
- robust [SMTP](https://github.com/chatmail/async-imap)
|
||||
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||
|
||||
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
||||
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
||||
@@ -167,7 +167,7 @@ $ cargo test -- --ignored
|
||||
|
||||
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
||||
```sh
|
||||
$ cargo install --locked cargo-bolero
|
||||
$ cargo install cargo-bolero
|
||||
```
|
||||
|
||||
Run fuzzing tests with
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, 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)".
|
||||
// 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();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
@@ -453,9 +453,18 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* always auto-downloaded.
|
||||
* 0 = no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
* and older Delta Chat versions.
|
||||
* 1 = enable.
|
||||
* 0 = disable (default).
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* This is deprecated, UIs should not behave differently
|
||||
* for chatmail relays and classical email servers.
|
||||
* Most usages in UIs can be replaced by `force_encryption`.
|
||||
* - `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()`,
|
||||
@@ -517,10 +526,9 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
*
|
||||
* - `sys.version` = get the version string e.g. as `1.2.3` or as `1.2.3special4`.
|
||||
* - `sys.msgsize_max_recommended` = maximal recommended attachment size in bytes.
|
||||
* All possible overheads are already subtracted and this value can be used
|
||||
* e.g. for direct comparison with the size of a file the user wants to attach.
|
||||
* If an attachment is larger than this value, the message is sent anyway,
|
||||
* but email servers are likely to reject the message when receiving it or before trying to send.
|
||||
* All possible overheads are already subtracted and this value can be used e.g. for direct comparison
|
||||
* with the size of a file the user wants to attach. If an attachment is larger than this value,
|
||||
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
||||
* - `sys.config_keys` = get a space-separated list of all config-keys available.
|
||||
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
||||
*
|
||||
@@ -688,21 +696,6 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
void dc_configure (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Add fake transport that cannot be used to connect.
|
||||
*
|
||||
* Used for offline tests only.
|
||||
*
|
||||
* To add a transport, use JSON-RPC calls `add_or_update_transport`
|
||||
* and `add_transport_from_qr` instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param addr The email address of the new transport.
|
||||
*/
|
||||
void dc_add_pseudo_transport (dc_context_t* context, const char *addr);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the context is already configured.
|
||||
*
|
||||
@@ -1784,6 +1777,8 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2282,7 +2277,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
|
||||
/**
|
||||
* Get encryption info for a contact.
|
||||
* Get a multi-line encryption info, containing your fingerprint and the
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -2449,7 +2444,7 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
|
||||
void dc_stop_ongoing_process (dc_context_t* context);
|
||||
|
||||
|
||||
// securejoin
|
||||
// out-of-band verification
|
||||
|
||||
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
|
||||
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
|
||||
@@ -2484,7 +2479,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* The QR code state is returned in dc_lot_t::state as:
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
|
||||
* ask whether to start chatting with the contact;
|
||||
* ask whether to verify the contact;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
@@ -2493,7 +2488,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
|
||||
* contact fingerprint matches,
|
||||
* contact fingerprint verified,
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
@@ -2569,24 +2564,23 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer a SecureJoin invitation.
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if dc_check_qr() returns
|
||||
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
* the SecureJoin protocol can be started using dc_join_securejoin()
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
*
|
||||
* The returned text will also work as a normal https:-link,
|
||||
* so that the QR code is useful also without Delta Chat being installed
|
||||
* or can be passed to contacts through other channels.
|
||||
*
|
||||
* To reset invitations, pass the link to dc_set_config_from_qr().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
* the SecureJoin QR code for the group is returned.
|
||||
* If set to 0, the setup contact QR code is returned.
|
||||
* 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/
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
@@ -2612,7 +2606,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Continue the SecureJoin protocol
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
* This function is typically called when dc_check_qr() returns
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
|
||||
@@ -3193,33 +3187,19 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
|
||||
/**
|
||||
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
|
||||
*
|
||||
* For an account with IO stopped, the scheduler is paused
|
||||
* and every transport is fetched concurrently on a dedicated connection.
|
||||
* The account is done as soon as one transport received messages, the others stop.
|
||||
* Only one batch of messages is fetched per transport this way,
|
||||
* so a larger backlog is left to the next call or to started IO.
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
*
|
||||
* For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
* and the account is done once every transport is.
|
||||
*
|
||||
* The call never waits for outgoing messages and never triggers sending them itself.
|
||||
* Received messages may still queue replies, securejoin handshakes for example,
|
||||
* which go out only while IO is running.
|
||||
*
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end,
|
||||
* also on timeout, when another background fetch is already running
|
||||
* and when the call is ignored because the timeout is too small,
|
||||
* so it is safe to wait for the event whenever `accounts` is not NULL.
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
|
||||
* even in case of timeout, unless the function fails and returns 0.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create a generic notification if no message was fetched.
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @param timeout The timeout in seconds
|
||||
* @return Return 0 if the call was ignored because `accounts` is NULL or the timeout is too small, 1 otherwise.
|
||||
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||
*/
|
||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||
|
||||
@@ -3589,6 +3569,7 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
|
||||
* last-message-state: @ref DC_STATE constant
|
||||
* last-message-date:
|
||||
* avatar-path: path-to-blobfile
|
||||
* is_verified: yes/no
|
||||
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
|
||||
@@ -4927,6 +4908,7 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
* By default, these names are equal,
|
||||
* but functions working with contact names
|
||||
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
|
||||
* dc_contact_get_name_n_addr(),
|
||||
* dc_create_contact() or dc_add_address_book())
|
||||
* only affect the given-name.
|
||||
*/
|
||||
@@ -4976,7 +4958,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
|
||||
* The function does not return the contact name as received from the network.
|
||||
*
|
||||
* This name is typically used in a form where the user can edit the name of a contact.
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5031,6 +5013,23 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
|
||||
#define dc_contact_get_first_name dc_contact_get_display_name
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary of name and address.
|
||||
*
|
||||
* The returned string is either "Name (email@domain.com)" or just
|
||||
* "email@domain.com" if the name is unset.
|
||||
*
|
||||
* The summary is typically used when asking the user something about the contact.
|
||||
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return A summary string, must be released using dc_str_unref().
|
||||
* Never returns NULL.
|
||||
*/
|
||||
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Get the contact's profile image.
|
||||
* This is the image set by each remote user on their own
|
||||
@@ -5110,6 +5109,19 @@ int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Returns whether contact is a bot.
|
||||
*
|
||||
@@ -5134,6 +5146,36 @@ int dc_contact_is_bot (dc_contact_t* contact);
|
||||
int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name of the contact
|
||||
* formatted by dc_contact_get_name().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_lot_t
|
||||
*
|
||||
@@ -6164,7 +6206,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
|
||||
* @param data2 0
|
||||
@@ -6236,7 +6278,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
*
|
||||
* @param data1 (int) The ID of the inviting contact.
|
||||
* @param data2 (int) The progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
* 1000=vg-member-added/vc-contact-confirm received
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
@@ -6319,18 +6362,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
|
||||
|
||||
/**
|
||||
* Tells that a call to dc_accounts_background_fetch() is done:
|
||||
* the fetch completed, timed out, was stopped or was not started.
|
||||
*
|
||||
* For the call that started the fetch, this event acts as a marker:
|
||||
* when you reach it, all events emitted during the fetch were processed.
|
||||
* A call made while another background fetch is running gets the event immediately,
|
||||
* and the running fetch keeps emitting events until its own marker.
|
||||
*
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
* Your own call has finished when dc_accounts_background_fetch() returns.
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
* This event acts as a marker, when you reach this event you can be sure
|
||||
* that all events emitted during the background fetch were processed.
|
||||
*
|
||||
* This event is only emitted by the account manager
|
||||
*/
|
||||
|
||||
@@ -6426,7 +6462,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
|
||||
* Moreover, the event is sent when the call was not accepted within two minutes.
|
||||
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
|
||||
*
|
||||
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
|
||||
*
|
||||
@@ -6438,10 +6474,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Transport relay added/deleted or default has changed.
|
||||
* UI should update the list.
|
||||
*
|
||||
* The event is emitted on the device modifying the transports
|
||||
* as well as on other devices applying the synced change,
|
||||
* for the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`,
|
||||
* `delete_transport` or `set_config(configured_addr)`.
|
||||
* The event is emitted when the transports are modified on another device
|
||||
* using the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`, `delete_transport`,
|
||||
* `set_transport_unpublished` or `set_config(configured_addr)`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
@@ -6626,12 +6661,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
#define DC_STR_FINGERPRINTS 30
|
||||
|
||||
/// "%1$s verified"
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the verified contact
|
||||
#define DC_STR_CONTACT_VERIFIED 35
|
||||
|
||||
/// "Archived chats"
|
||||
///
|
||||
/// Used as the name for the corresponding chatlist entry.
|
||||
#define DC_STR_ARCHIVEDCHATS 40
|
||||
|
||||
/// @deprecated 2026-08-24
|
||||
/// "Cannot login as %1$s."
|
||||
///
|
||||
/// Used in error strings.
|
||||
/// - %1$s will be replaced by the failing login name
|
||||
#define DC_STR_CANNOT_LOGIN 60
|
||||
|
||||
/// "Location streaming enabled."
|
||||
@@ -6804,7 +6848,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Added as an info-message directly after scanning a QR code for joining a group.
|
||||
/// May be followed by the info-messages
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES and #DC_STR_MSGADDMEMBER.
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
|
||||
///
|
||||
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_STARTED 117
|
||||
@@ -6813,13 +6857,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Info-message on scanning a QR code for joining a group.
|
||||
/// Added after #DC_STR_SECURE_JOIN_STARTED.
|
||||
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
|
||||
/// this info-message is skipped.
|
||||
///
|
||||
/// `%1$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_REPLIES 118
|
||||
|
||||
/// "Scan to chat with %1$s"
|
||||
///
|
||||
/// Subtitle for the invite qrcode svg image generated by the core.
|
||||
/// Subtitle for verification qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SETUP_CONTACT_QR_DESC 119
|
||||
@@ -7239,7 +7285,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Message pinned by %1$s."
|
||||
#define DC_STR_MESSAGE_PINNED_BY_OTHER 244
|
||||
|
||||
/// @deprecated 2026-08-31
|
||||
/// "Phasing out"
|
||||
///
|
||||
/// Used in connectivity view to flag unpublished relays.
|
||||
/// This should match the wording used for relay deletion confirmation,
|
||||
/// saying "Before deletion, it will be gradually phased out so your contacts can switch over smoothly"
|
||||
#define DC_STR_PHASING_OUT 245
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||
use crate::contact::ContactId;
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
@@ -20,7 +21,7 @@ impl dc_array_t {
|
||||
Self::ContactIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
Self::Uint(array) => array[index],
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::{Context, ContextBuilder};
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
@@ -31,7 +32,6 @@ use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::transport::add_pseudo_transport;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -415,21 +415,6 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||
spawn_configure(ctx.clone());
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_add_pseudo_transport(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_add_pseudo_transport()");
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = unsafe { &*context };
|
||||
let addr = to_string_lossy(addr);
|
||||
block_on(add_pseudo_transport(ctx, &addr)).log_err(ctx).ok();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -706,7 +691,7 @@ pub unsafe extern "C" fn dc_event_get_data1_str(event: *mut dc_event_t) -> *mut
|
||||
match event {
|
||||
EventType::IncomingWebxdcNotify { href, .. } => {
|
||||
if let Some(href) = href {
|
||||
href.strdup()
|
||||
href.to_c_string().unwrap_or_default().into_raw()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
@@ -735,7 +720,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::DeletedBlobFile(msg)
|
||||
| EventType::Warning(msg)
|
||||
| EventType::Error(msg)
|
||||
| EventType::ErrorSelfNotInGroup(msg) => msg.strdup(),
|
||||
| EventType::ErrorSelfNotInGroup(msg) => {
|
||||
let data2 = msg.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::ReactionsChanged { .. }
|
||||
| EventType::IncomingMsg { .. }
|
||||
@@ -769,27 +757,45 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::TransportsModified => ptr::null_mut(),
|
||||
EventType::IncomingCall {
|
||||
place_call_info, ..
|
||||
} => place_call_info.strdup(),
|
||||
} => {
|
||||
let data2 = place_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::OutgoingCallAccepted {
|
||||
accept_call_info, ..
|
||||
} => accept_call_info.strdup(),
|
||||
} => {
|
||||
let data2 = accept_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.strdup()
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
EventType::ImexFileWritten(file) => file.strdup(),
|
||||
EventType::ConfigSynced { key } => key.to_string().strdup(),
|
||||
EventType::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::ConfigSynced { key } => {
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::WebxdcRealtimeData { data, .. } => {
|
||||
let ptr = unsafe { libc::malloc(data.len()) };
|
||||
unsafe { libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len()) };
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
EventType::IncomingReaction { reaction, .. } => reaction.as_str().strdup(),
|
||||
EventType::IncomingWebxdcNotify { text, .. } => text.strdup(),
|
||||
EventType::IncomingReaction { reaction, .. } => reaction
|
||||
.as_str()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
EventType::IncomingWebxdcNotify { text, .. } => {
|
||||
text.to_c_string().unwrap_or_default().into_raw()
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
@@ -1768,7 +1774,8 @@ pub unsafe extern "C" fn dc_set_chat_name(
|
||||
chat_id: u32,
|
||||
name: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() || name.is_null() {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null()
|
||||
{
|
||||
eprintln!("ignoring careless call to dc_set_chat_name()");
|
||||
return 0;
|
||||
}
|
||||
@@ -1789,7 +1796,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
|
||||
chat_id: u32,
|
||||
image: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_profile_image()");
|
||||
return 0;
|
||||
}
|
||||
@@ -1951,7 +1958,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
if context.is_null()
|
||||
|| msg_ids.is_null()
|
||||
|| msg_cnt <= 0
|
||||
|| chat_id <= ChatId::LAST_SPECIAL.to_u32()
|
||||
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32()
|
||||
{
|
||||
eprintln!("ignoring careless call to dc_forward_msgs()");
|
||||
return;
|
||||
@@ -2032,7 +2039,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(_) => {
|
||||
if MsgId::new(msg_id).is_special() {
|
||||
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
|
||||
// C-core API returns empty messages, do the same
|
||||
message::Message::new(Viewtype::default())
|
||||
} else {
|
||||
@@ -2413,7 +2420,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_securejoin_qr_svg()");
|
||||
eprintln!("ignoring careless call to generate_verification_qr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = unsafe { &*context };
|
||||
@@ -2455,7 +2462,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
chat_id: u32,
|
||||
seconds: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || chat_id <= ChatId::LAST_SPECIAL.to_u32() || seconds < 0 {
|
||||
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 {
|
||||
eprintln!("ignoring careless call to dc_send_locations_to_chat()");
|
||||
return;
|
||||
}
|
||||
@@ -3979,6 +3986,18 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
|
||||
ffi_contact.contact.get_display_name().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_name_n_addr(
|
||||
contact: *mut dc_contact_t,
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.get_name_n_addr().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_profile_image(
|
||||
contact: *mut dc_contact_t,
|
||||
@@ -4053,6 +4072,27 @@ pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> li
|
||||
ffi_contact.contact.is_blocked() as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_verified()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
|
||||
if block_on(ffi_contact.contact.is_verified(&ffi_contact.context))
|
||||
.context("is_verified failed")
|
||||
.log_err(&ffi_contact.context)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Return value is essentially a boolean,
|
||||
// but we return 2 for true for backwards compatibility.
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
@@ -4071,6 +4111,22 @@ pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -
|
||||
unsafe { (*contact).contact.is_key_contact() as libc::c_int }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_verifier_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(&ffi_contact.context))
|
||||
.context("failed to get verifier")
|
||||
.log_err(&ffi_contact.context)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
verifier_contact_id.to_u32()
|
||||
}
|
||||
// dc_lot_t
|
||||
|
||||
pub type dc_lot_t = lot::Lot;
|
||||
@@ -4352,7 +4408,7 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
|
||||
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
|
||||
let msg_ids: Vec<MsgId> = ids
|
||||
.iter()
|
||||
.filter(|id| **id > MsgId::LAST_SPECIAL.to_u32())
|
||||
.filter(|id| **id > DC_MSG_ID_LAST_SPECIAL)
|
||||
.map(|id| MsgId::new(*id))
|
||||
.collect();
|
||||
|
||||
@@ -4717,17 +4773,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
accounts: *const dc_accounts_t,
|
||||
timeout_in_seconds: u64,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = unsafe { &*accounts };
|
||||
if timeout_in_seconds <= 2 {
|
||||
eprintln!("ignoring careless call to dc_accounts_background_fetch(): timeout too small");
|
||||
block_on(accounts.read()).emit_event(EventType::AccountsBackgroundFetchDone);
|
||||
return 0;
|
||||
}
|
||||
let background_fetch_future = {
|
||||
let lock = block_on(accounts.read());
|
||||
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[build-dependencies]
|
||||
deltachat-jsonrpc = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
vendored = ["deltachat-jsonrpc/vendored"]
|
||||
@@ -1,7 +0,0 @@
|
||||
use deltachat_jsonrpc::api::{write_qt_bindings, write_ts_bindings};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
write_ts_bindings(Path::new("typescript/generated"));
|
||||
write_qt_bindings(Path::new("qt/generated"), "deltachat");
|
||||
}
|
||||
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
1
deltachat-jsonrpc-bindings/qt/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
generated
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "deltachat.h"
|
||||
#include "generated/client.hpp"
|
||||
#include "generated/types.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace deltachat {
|
||||
|
||||
class CffiTransport : public Transport {
|
||||
using CompletionHandler = Transport::CompletionHandler;
|
||||
|
||||
public:
|
||||
explicit CffiTransport(dc_accounts_t *accounts)
|
||||
: jsonrpc_(dc_jsonrpc_init(accounts)) {
|
||||
if (!jsonrpc_)
|
||||
std::abort();
|
||||
thread_ = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
~CffiTransport() override {
|
||||
done_ = true;
|
||||
// Unblock dc_jsonrpc_next_response by sending a dummy request
|
||||
if (jsonrpc_)
|
||||
dc_jsonrpc_request(
|
||||
jsonrpc_,
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
|
||||
if (thread_.joinable())
|
||||
thread_.join();
|
||||
std::lock_guard lk(mu_);
|
||||
for (auto &[id, cb] : pending_) {
|
||||
cb(Result<QJsonValue>::error(-32060, "Transport destructed"));
|
||||
}
|
||||
pending_.clear();
|
||||
if (jsonrpc_)
|
||||
dc_jsonrpc_unref(jsonrpc_);
|
||||
}
|
||||
|
||||
virtual void send(const QString method, const QJsonValue params,
|
||||
CompletionHandler onCompleted) override {
|
||||
uint32_t id = next_id_++;
|
||||
QJsonObject envelope{
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"method", method},
|
||||
{"params", params},
|
||||
};
|
||||
|
||||
{
|
||||
std::lock_guard lk(mu_);
|
||||
pending_[id] = std::move(onCompleted);
|
||||
}
|
||||
|
||||
QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
|
||||
dc_jsonrpc_request(jsonrpc_, json.constData());
|
||||
}
|
||||
|
||||
private:
|
||||
void run() {
|
||||
while (!done_) {
|
||||
char *raw_json = dc_jsonrpc_next_response(jsonrpc_);
|
||||
if (!raw_json) {
|
||||
break;
|
||||
}
|
||||
QByteArray json{raw_json};
|
||||
dc_str_unref(raw_json);
|
||||
if (done_)
|
||||
break;
|
||||
|
||||
QJsonObject obj = QJsonDocument::fromJson(json).object();
|
||||
|
||||
if (!obj["id"].isDouble()) {
|
||||
qCritical() << "No valid rpc id in" << QString{json};
|
||||
continue;
|
||||
}
|
||||
uint32_t id = static_cast<uint32_t>(obj["id"].toInt());
|
||||
|
||||
CompletionHandler cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (auto nh = pending_.extract(id)) {
|
||||
cb = std::move(nh.mapped());
|
||||
} else {
|
||||
qCritical() << "Could not map response" << QString{json};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
cb(parseResult(obj));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
dc_jsonrpc_instance_t *jsonrpc_;
|
||||
std::thread thread_;
|
||||
std::mutex mu_;
|
||||
std::atomic<uint32_t> next_id_{1};
|
||||
std::atomic<bool> done_{false};
|
||||
std::unordered_map<uint32_t, CompletionHandler> pending_;
|
||||
};
|
||||
|
||||
class CffiDeltaChat : public RawClient {
|
||||
public:
|
||||
explicit CffiDeltaChat(dc_accounts_t *accounts)
|
||||
: RawClient(std::make_unique<CffiTransport>(accounts)) {}
|
||||
};
|
||||
|
||||
} // namespace deltachat
|
||||
Q_DECLARE_METATYPE(deltachat::CffiDeltaChat *)
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ use deltachat::chat::{
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{Config, get_all_ui_config_keys};
|
||||
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
@@ -65,6 +66,7 @@ use self::types::{
|
||||
};
|
||||
use crate::api::types::appversions::JsonrpcAppSource;
|
||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -153,7 +155,7 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional)]
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -278,26 +280,9 @@ impl CommandApi {
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// For an account with IO stopped, the scheduler is paused
|
||||
/// and every transport is fetched concurrently on a dedicated connection.
|
||||
/// The account is done as soon as one transport received messages, the others stop.
|
||||
/// Only one batch of messages is fetched per transport this way,
|
||||
/// so a larger backlog is left to the next call or to started IO.
|
||||
///
|
||||
/// For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
/// and the account is done once every transport is.
|
||||
///
|
||||
/// The call never waits for outgoing messages and never triggers sending them itself.
|
||||
/// Received messages may still queue replies, securejoin handshakes for example,
|
||||
/// which go out only while IO is running.
|
||||
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
|
||||
/// and immediately if another background fetch is already running.
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create a generic notification if no message was fetched.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it marks your own call only if no concurrent background fetch is happening.
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
@@ -308,11 +293,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops an ongoing `background_fetch()` call, making it return early
|
||||
/// without waiting for the remaining transports or for the timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
|
||||
/// Does nothing if no background fetch is running.
|
||||
async fn stop_background_fetch(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_background_fetch();
|
||||
Ok(())
|
||||
@@ -523,7 +503,8 @@ impl CommandApi {
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to remove a transport.
|
||||
/// - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -548,8 +529,32 @@ impl CommandApi {
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to remove a transport.
|
||||
/// and [Self::set_transport_unpublished()] to remove a transport.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|t| !t.is_unpublished)
|
||||
.map(|t| t.param.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Deprecated 2026-06: This is not needed by UI implementations anymore,
|
||||
/// because unpublished relays now count as removed from the user point of view,
|
||||
/// and must not be shown in the list of relays.
|
||||
/// This means that UIs should use `list_transports()` instead of this function.
|
||||
///
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
///
|
||||
/// As opposed to `list_transports()`, this function also returns unpublished transports,
|
||||
/// and for each returned transport it returns the information whether or not is `unpublished`.
|
||||
///
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::set_transport_unpublished()] to change whether a transport is 'published'.
|
||||
async fn list_transports_ex(&self, account_id: u32) -> Result<Vec<TransportListEntry>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
@@ -560,17 +565,41 @@ impl CommandApi {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Removes a transport.
|
||||
/// UIs should call this function when the user removes a relay.
|
||||
///
|
||||
/// The last transport cannot be removed.
|
||||
/// If the removed transport was the one used for sending,
|
||||
/// another one is chosen automatically.
|
||||
/// Immediately deletes a transport, potentially causing messages not to arrive.
|
||||
/// This must ONLY be used by the automated tests.
|
||||
/// UI implementations must use [`Self::set_transport_unpublished`] instead.
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Change whether the transport is unpublished.
|
||||
/// UIs should call this function when the user clicks on "Remove".
|
||||
/// Core will keep listening on this transport for some time,
|
||||
/// and automatically remove it once it is no longer needed.
|
||||
///
|
||||
/// Unpublished transports are not advertised to contacts,
|
||||
/// and self-sent messages are not sent there,
|
||||
/// so that we don't cause extra messages to the corresponding inbox,
|
||||
/// but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
///
|
||||
/// When more transports are added by [`Self::add_or_update_transport()`] or [`Self::add_transport_from_qr`],
|
||||
/// the least recently needed unpublished transport is automatically removed
|
||||
/// if this is necessary in order to stay below the maximum number of allowed relays.
|
||||
/// Also, unpublished transports that are not used to receive any new messages for a time defined by
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`] are automatically removed.
|
||||
///
|
||||
/// [`UNPUBLISHED_TRANSPORT_KEEP_TIME`]: deltachat::sql::UNPUBLISHED_TRANSPORT_KEEP_TIME
|
||||
async fn set_transport_unpublished(
|
||||
&self,
|
||||
account_id: u32,
|
||||
addr: String,
|
||||
unpublished: bool,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.set_transport_unpublished(&addr, unpublished).await
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -847,8 +876,6 @@ impl CommandApi {
|
||||
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// To reset invitations, pass the link to `set_config_from_qr()`.
|
||||
///
|
||||
/// 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(
|
||||
@@ -862,19 +889,20 @@ impl CommandApi {
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer a SecureJoin invitation.
|
||||
/// Get QR code (text and SVG) that will offer a 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.
|
||||
///
|
||||
/// The scanning device will pass the scanned content to `checkQr()` then;
|
||||
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
/// the securejoin protocol can be started using `secure_join()`
|
||||
/// an out-of-band-verification can be joined using `secure_join()`
|
||||
///
|
||||
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||
///
|
||||
/// chat_id: If set to a group-chat-id,
|
||||
/// the SecureJoin QR code for the group is returned.
|
||||
/// If not set, the setup contact QR code is returned.
|
||||
/// 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.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
@@ -890,7 +918,7 @@ impl CommandApi {
|
||||
Ok((qr, svg))
|
||||
}
|
||||
|
||||
/// Continue the SecureJoin protocol
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
/// This function is typically called when `check_qr()` returns
|
||||
/// type=AskVerifyContact or type=AskVerifyGroup.
|
||||
@@ -973,6 +1001,8 @@ impl CommandApi {
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
async fn add_contact_to_chat(
|
||||
&self,
|
||||
@@ -1352,7 +1382,7 @@ impl CommandApi {
|
||||
///
|
||||
/// * chat_id The chat ID of which the messages IDs should be queried.
|
||||
/// * _info_only: Deprecated, pass `false` here.
|
||||
/// * `add_daymarker` - If `true`, add day markers as `MsgId::DAYMARKER` to the result,
|
||||
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
/// corresponding (following) day in the local timezone.
|
||||
async fn get_message_ids(
|
||||
@@ -1374,7 +1404,7 @@ impl CommandApi {
|
||||
.map(|chat_item| -> u32 {
|
||||
match chat_item {
|
||||
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
deltachat::chat::ChatItem::DayMarker { .. } => MsgId::DAYMARKER.to_u32(),
|
||||
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -1794,7 +1824,7 @@ impl CommandApi {
|
||||
|
||||
/// Get encryption info for a contact.
|
||||
/// Get a multi-line encryption info, containing your fingerprint and the
|
||||
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
async fn get_contact_encryption_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2059,14 +2089,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Waits until all transports are idle or failed and no background work is left.
|
||||
/// Never returns unless I/O is started. Must ONLY be used by tests.
|
||||
async fn wait_for_all_work_done(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.wait_for_all_work_done().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||
/// One of:
|
||||
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000): Show e.g. the string "Not connected" or a red dot
|
||||
@@ -2256,9 +2278,6 @@ impl CommandApi {
|
||||
/// Get blob encoded as base64 from a webxdc message
|
||||
///
|
||||
/// path is the path of the file within webxdc archive
|
||||
///
|
||||
/// If the file is `icon.png` or `icon.jpg`,
|
||||
/// loading it may fail if dimensions are unexpectedly large.
|
||||
async fn get_webxdc_blob(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2439,7 +2458,6 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns reactions to the message.
|
||||
/// `None` when there are no reactions.
|
||||
async fn get_message_reactions(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2804,15 +2822,6 @@ impl CommandApi {
|
||||
.map(JsonrpcAppSource::from_core_type),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if all accounts have empty outgoing message queue.
|
||||
///
|
||||
/// This API is intended to be used by UIs
|
||||
/// to request that operating system does not put the application in background
|
||||
/// while there are still outgoing messages that are not sent out.
|
||||
async fn is_sending_finished(&self) -> Result<bool> {
|
||||
self.accounts.read().await.is_sending_finished().await
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (to prevent code duplication)
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct ContactObject {
|
||||
id: u32,
|
||||
name: String,
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// Is the contact a key contact.
|
||||
@@ -30,6 +31,37 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// UI should display the information in the contact's profile as follows:
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name of the contact.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
/// display "Introduced" prefixed by a green checkmark.
|
||||
///
|
||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||
/// display nothing
|
||||
///
|
||||
/// This contains the contact ID of the verifier.
|
||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||
/// If it is None/Null, we don't have verifier information or
|
||||
/// the contact is not verified.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
@@ -47,6 +79,14 @@ impl ContactObject {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await?;
|
||||
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
address: contact.get_addr().to_owned(),
|
||||
color: color_int_to_hex_string(contact.get_color()),
|
||||
@@ -56,9 +96,12 @@ impl ContactObject {
|
||||
id: contact.id.to_u32(),
|
||||
name: contact.get_name().to_owned(),
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
is_bot: contact.is_bot(),
|
||||
|
||||
@@ -337,7 +337,8 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
|
||||
/// Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: u16,
|
||||
},
|
||||
@@ -393,15 +394,11 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Tells that a background fetch call is done:
|
||||
/// the fetch completed, timed out, was stopped or was not started.
|
||||
/// Tells that the Background fetch was completed (or timed out).
|
||||
/// This event acts as a marker, when you reach this event you can be sure
|
||||
/// that all events emitted during the background fetch were processed.
|
||||
///
|
||||
/// For the call that started the fetch, this event acts as a marker:
|
||||
/// all events emitted during the fetch were processed once it is reached.
|
||||
/// A call made while another background fetch is running gets the event immediately,
|
||||
/// and the running fetch keeps emitting events until its own marker.
|
||||
///
|
||||
/// This event is only emitted by the account manager.
|
||||
/// 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.
|
||||
///
|
||||
@@ -481,9 +478,9 @@ pub enum EventType {
|
||||
///
|
||||
/// UI should update the list.
|
||||
///
|
||||
/// The event is emitted on the device modifying
|
||||
/// the transports as well as on other devices
|
||||
/// applying the synced change.
|
||||
/// This event is emitted when transport
|
||||
/// synchronization messages arrives,
|
||||
/// but not when the UI modifies the transport list by itself.
|
||||
TransportsModified,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use yerpc::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransportListEntry {
|
||||
/// The login data entered by the user.
|
||||
pub param: EnteredLoginParam,
|
||||
/// Whether this transport is set to 'unpublished'.
|
||||
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
|
||||
pub is_unpublished: bool,
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
@@ -58,6 +68,15 @@ pub struct EnteredLoginParam {
|
||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||
}
|
||||
|
||||
impl From<dc::TransportListEntry> for TransportListEntry {
|
||||
fn from(transport: dc::TransportListEntry) -> Self {
|
||||
TransportListEntry {
|
||||
param: transport.param.into(),
|
||||
is_unpublished: transport.is_unpublished,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||
let imap_security: Socket = param.imap.security.into();
|
||||
|
||||
@@ -105,7 +105,6 @@ pub struct MessageObject {
|
||||
|
||||
is_pinned: bool,
|
||||
|
||||
/// `None` when there are no reactions.
|
||||
reactions: Option<JsonrpcReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum QrObject {
|
||||
/// Ask the user whether to start chatting with the contact.
|
||||
/// Ask the user whether to verify the contact.
|
||||
///
|
||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||
AskVerifyContact {
|
||||
@@ -61,7 +61,7 @@ pub enum QrObject {
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Contact fingerprint matches.
|
||||
/// Contact fingerprint is verified.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.60.0-dev"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -27,6 +27,55 @@ use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -179,7 +228,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
let mut lines_out = 0;
|
||||
for &msg_id in msglist {
|
||||
if msg_id == MsgId::DAYMARKER {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
@@ -210,13 +259,19 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let verified_str = if contact.is_verified(context).await? {
|
||||
" √"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let line = format!(
|
||||
"{} <{}>",
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() { addr } else { "addr unset" }
|
||||
);
|
||||
|
||||
@@ -255,6 +310,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -394,6 +450,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
@@ -565,7 +630,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::DayMarker { .. } => MsgId::DAYMARKER,
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1054,11 +1119,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {name} ({addr}):\nIcon: {}\n",
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,6 +156,7 @@ const IMEX_COMMANDS: [&str; 9] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
@@ -172,7 +173,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 38] = [
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -181,6 +182,7 @@ const CHAT_COMMANDS: [&str; 38] = [
|
||||
"createchat",
|
||||
"creategroup",
|
||||
"createbroadcast",
|
||||
"createprotected",
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -143,6 +143,10 @@ class Account:
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
|
||||
def set_transport_unpublished(self, addr: str, unpublished: bool = True):
|
||||
"""Unpublish the transport."""
|
||||
self._rpc.set_transport_unpublished(self.id, addr, unpublished)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
@@ -150,10 +154,9 @@ class Account:
|
||||
return transports
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O, wait until all transports became IDLE and drop the events seen so far."""
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
self._rpc.wait_for_all_work_done(self.id)
|
||||
self.clear_all_events()
|
||||
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
@@ -272,7 +275,7 @@ class Account:
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Contact:
|
||||
def device_contact(self) -> Chat:
|
||||
"""Account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
@@ -360,7 +363,7 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue the SecureJoin protocol started on another device.
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
|
||||
@@ -70,7 +70,6 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
|
||||
@@ -48,13 +48,6 @@ class DeltaChat:
|
||||
"""Stop ongoing background fetch."""
|
||||
self.rpc.stop_background_fetch()
|
||||
|
||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||
"""Wait until the next account manager event and return it."""
|
||||
while True:
|
||||
next_event = AttrDict(self.rpc.wait_for_event(0))
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
@@ -74,7 +67,3 @@ class DeltaChat:
|
||||
def stop_sending_locations(self) -> None:
|
||||
"""Stop sending locations to all chats."""
|
||||
return self.rpc.stop_sending_locations()
|
||||
|
||||
def is_sending_finished(self) -> bool:
|
||||
"""Return true if sending queues of all accounts are empty."""
|
||||
return self.rpc.is_sending_finished()
|
||||
|
||||
@@ -121,7 +121,7 @@ class Message:
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data):
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class RPCAccountFactory:
|
||||
"""Create a new unconfigured bot."""
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def get_credentials(self) -> tuple[str, str]:
|
||||
def get_credentials(self) -> (str, str):
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
@@ -39,15 +38,8 @@ class RpcMethod:
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
queue: Queue = Queue()
|
||||
# Register before testing for shutdown, so that either the reader loop
|
||||
# finds this request while draining, or the test below catches it here.
|
||||
# Testing first would race with the reader loop finishing in between.
|
||||
self.rpc.request_results[request_id] = queue
|
||||
if self.rpc.request_queue_closed:
|
||||
self.rpc._fail_request(request_id)
|
||||
else:
|
||||
self.rpc.request_queue.put(request)
|
||||
self.rpc.request_results[request_id] = queue = Queue()
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
def rpc_future():
|
||||
"""Wait for the request to receive a result."""
|
||||
@@ -86,10 +78,6 @@ class Rpc:
|
||||
# Map from request ID to a Queue which provides a single result
|
||||
self.request_results: dict[int, Queue]
|
||||
self.request_queue: Queue[Any]
|
||||
# Emulates `request_queue.shutdown(immediate=False)`, which needs Python 3.13:
|
||||
# https://github.com/python/cpython/blob/v3.13.0/Lib/queue.py#L236-L257
|
||||
# Note that `request_queue_closed` is set by the reader loop.
|
||||
self.request_queue_closed: bool
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
self.writer_thread: Thread
|
||||
@@ -119,7 +107,6 @@ class Rpc:
|
||||
self.event_queues = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.request_queue_closed = False
|
||||
self.closing = False
|
||||
self.reader_thread = Thread(target=self.reader_loop)
|
||||
self.reader_thread.start()
|
||||
@@ -136,8 +123,6 @@ class Rpc:
|
||||
# The reader_loop already saw EOF on stdout, so the process
|
||||
# has exited and stderr is available.
|
||||
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
||||
self.closing = True
|
||||
self._shutdown_loops()
|
||||
if stderr:
|
||||
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
||||
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
||||
@@ -150,31 +135,11 @@ class Rpc:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
self.stop_io_for_all_accounts()
|
||||
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
|
||||
# otherwise it might exit through an "RPC server closed" error instead.
|
||||
self.events_thread.join()
|
||||
self._shutdown_loops()
|
||||
|
||||
def _shutdown_loops(self) -> None:
|
||||
"""Close the server pipe and wait for the loop threads to finish.
|
||||
|
||||
The writer blocks on an empty request queue,
|
||||
so it needs the sentinel to notice the shutdown.
|
||||
"""
|
||||
with contextlib.suppress(BrokenPipeError):
|
||||
# An exited server may leave data unflushed,
|
||||
# which close() would try to write out again.
|
||||
self.process.stdin.close()
|
||||
self.request_queue.put(None)
|
||||
self.process.stdin.close()
|
||||
self.reader_thread.join()
|
||||
self.request_queue.put(None)
|
||||
self.writer_thread.join()
|
||||
self.events_thread.join()
|
||||
|
||||
def _fail_request(self, request_id: int) -> None:
|
||||
"""Answer a registered request with an error, unless it was answered already."""
|
||||
queue = self.request_results.pop(request_id, None)
|
||||
if queue is not None:
|
||||
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
@@ -197,11 +162,9 @@ class Rpc:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
finally:
|
||||
# Shut the request queue first, so that requests registered from now
|
||||
# on are failed by their caller, then answer the pending ones here.
|
||||
self.request_queue_closed = True
|
||||
for request_id in list(self.request_results):
|
||||
self._fail_request(request_id)
|
||||
# Unblock any pending requests when the server closes stdout.
|
||||
for _request_id, queue in self.request_results.items():
|
||||
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
||||
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
|
||||
@@ -22,10 +22,8 @@ ALL = "1:*"
|
||||
class DirectImap:
|
||||
"""Internal Python-level IMAP handling."""
|
||||
|
||||
def __init__(self, account: Account, addr=None, password=None) -> None:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.addr = addr or account.get_config("addr")
|
||||
self.password = password or account.get_config("mail_pw")
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
@@ -35,9 +33,9 @@ class DirectImap:
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = 993
|
||||
|
||||
user = self.addr
|
||||
user = self.account.get_config("addr")
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.password
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
if host.startswith("_"):
|
||||
@@ -171,7 +169,7 @@ class DirectImap:
|
||||
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)), mark_seen=False)]
|
||||
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]
|
||||
@@ -180,6 +178,9 @@ class DirectImap:
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
# 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]:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import EventType, const
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
||||
@@ -144,7 +144,7 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
|
||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -25,12 +24,13 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
||||
# Test that Bob verified Alice's profile.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
# Test that Bob can also scan a QR code
|
||||
@@ -43,27 +43,6 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||
def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) -> None:
|
||||
"""Test other-core Bob profile can do securejoin with Alice on current core, with multiple transports."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
relay_qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(relay_qr)
|
||||
alice.add_transport_from_qr(relay_qr)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
|
||||
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.23.0")
|
||||
@@ -85,138 +64,3 @@ def test_second_device(acf, alice_and_remote_bob) -> None:
|
||||
remote_eval("locals()['future']()")
|
||||
|
||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("replace_relay", [False, True], ids=["add", "replace"])
|
||||
def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, replace_relay):
|
||||
"""Test 2.48 Bob learns a relay change of Alice from a keyupdate, and is shown nothing."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.48.0")
|
||||
|
||||
def bob_sees():
|
||||
return remote_eval(
|
||||
"{'chats': len(bob.get_chatlist()),"
|
||||
" 'fresh': len(bob._rpc.get_fresh_msgs(bob.id)),"
|
||||
" 'contacts': len(bob.get_contacts()),"
|
||||
" 'alice_chat': bob._rpc.get_chat_id_by_contact_id(bob.id, bob_contact_alice.id) or 0}",
|
||||
)
|
||||
|
||||
# Keyupdates go to contacts who plausibly hold our key:
|
||||
# an accepted chat alone is not enough, a message must have flowed.
|
||||
alice_chat = alice_contact_bob.create_chat()
|
||||
alice.set_config("keyupdate_debounce", "1")
|
||||
old_addr = alice.get_config("configured_addr")
|
||||
alice_chat.send_text("hi")
|
||||
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
|
||||
before = bob_sees()
|
||||
|
||||
# Certificate merging keeps the newest direct key signature,
|
||||
# and signature timestamps have one-second resolution:
|
||||
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
|
||||
time.sleep(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
|
||||
if replace_relay:
|
||||
alice.delete_transport(old_addr)
|
||||
alice.bring_online()
|
||||
|
||||
# The 2.48 core has no encryption enforcement, but the keyupdate MDN without
|
||||
# referenced message keeps it invisible; merging happens before the trashing.
|
||||
for _ in range(60):
|
||||
if new_addr in remote_eval("bob_contact_alice.get_encryption_info()"):
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
pytest.fail("Bob never received the keyupdate")
|
||||
|
||||
# It also leaves no trace: no chat with Alice, no message anywhere,
|
||||
# and no address-contact for the address it was sent from.
|
||||
assert bob_sees() == before
|
||||
|
||||
if replace_relay:
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello after replacement')")
|
||||
assert alice.wait_for_incoming_msg().get_snapshot().text == "hello after replacement"
|
||||
|
||||
|
||||
class LocalSide:
|
||||
"""Alice on the core under test."""
|
||||
|
||||
def __init__(self, account, peer_contact):
|
||||
self.account = account
|
||||
self.peer_contact = peer_contact
|
||||
self.chat = None
|
||||
|
||||
def make_qr(self, invite):
|
||||
if invite == "group":
|
||||
self.chat = self.account.create_group("Group")
|
||||
elif invite == "broadcast":
|
||||
self.chat = self.account.create_broadcast("Channel")
|
||||
return self.chat.get_qr_code() if self.chat else self.account.get_qr_code()
|
||||
|
||||
def join(self, qr):
|
||||
self.account.secure_join(qr)
|
||||
|
||||
def wait_inviter(self):
|
||||
self.account.wait_for_securejoin_inviter_success()
|
||||
|
||||
def wait_joiner(self):
|
||||
self.account.wait_for_securejoin_joiner_success()
|
||||
|
||||
def send_text(self, text):
|
||||
chat = self.chat or self.peer_contact.create_chat()
|
||||
chat.send_text(text)
|
||||
|
||||
def next_text(self):
|
||||
return self.account.wait_for_incoming_msg().get_snapshot().text
|
||||
|
||||
|
||||
class RemoteSide:
|
||||
"""Bob on the other core, driven through remote_eval."""
|
||||
|
||||
def __init__(self, remote_eval):
|
||||
self.remote_eval = remote_eval
|
||||
self.chat = None
|
||||
|
||||
def make_qr(self, invite):
|
||||
if invite == "contact":
|
||||
return self.remote_eval("bob.get_qr_code()")
|
||||
create = {"group": "bob.create_group('Group')", "broadcast": "bob.create_broadcast('Channel')"}[invite]
|
||||
self.remote_eval(f"locals().update(chat={create})")
|
||||
self.chat = "chat"
|
||||
return self.remote_eval("chat.get_qr_code()")
|
||||
|
||||
def join(self, qr):
|
||||
self.remote_eval(f"bob.secure_join({qr!r})")
|
||||
|
||||
def wait_inviter(self):
|
||||
self.remote_eval("bob.wait_for_securejoin_inviter_success()")
|
||||
|
||||
def wait_joiner(self):
|
||||
self.remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
def send_text(self, text):
|
||||
chat = self.chat or "bob_contact_alice.create_chat()"
|
||||
self.remote_eval(f"{chat}.send_text({text!r})")
|
||||
|
||||
def next_text(self):
|
||||
return self.remote_eval("bob.wait_for_incoming_msg().get_snapshot().text")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.48.0"])
|
||||
@pytest.mark.parametrize("invite", ["contact", "group", "broadcast"])
|
||||
@pytest.mark.parametrize("remote_invites", [False, True], ids=["local-invites", "remote-invites"])
|
||||
def test_securejoin_invite(alice_and_remote_bob, version, invite, remote_invites):
|
||||
"""Every invite link type works with either core as the inviter."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
local = LocalSide(alice, alice_contact_bob)
|
||||
remote = RemoteSide(remote_eval)
|
||||
inviter, joiner = (remote, local) if remote_invites else (local, remote)
|
||||
|
||||
joiner.join(inviter.make_qr(invite))
|
||||
joiner.wait_joiner()
|
||||
inviter.wait_inviter()
|
||||
# Group and broadcast joins add an info message first.
|
||||
if invite != "contact":
|
||||
joiner.next_text()
|
||||
|
||||
inviter.send_text("hello")
|
||||
assert joiner.next_text() == "hello"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
@@ -8,22 +5,6 @@ from deltachat_rpc_client.const import ChatType, DownloadState
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def alice_with_two_transports_and_bob(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
return alice, alice.create_chat(bob), bob.create_chat(alice)
|
||||
|
||||
|
||||
def messages_with_text(chat, text):
|
||||
return [msg for msg in chat.get_messages() if msg.get_snapshot().text == text]
|
||||
|
||||
|
||||
def wait_for_imap_message(imap):
|
||||
while not imap.get_all_messages():
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_add_second_address(acf) -> None:
|
||||
account = acf.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
@@ -37,24 +18,20 @@ def test_add_second_address(acf) -> None:
|
||||
|
||||
first_addr = account.list_transports()[0]["addr"]
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
|
||||
assert account.get_config("configured_addr") == first_addr
|
||||
account.delete_transport(first_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
assert account.get_config("configured_addr") != first_addr
|
||||
# Cannot delete the first address.
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(first_addr)
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.delete_transport(third_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
|
||||
def test_change_address(acf) -> None:
|
||||
"""Test Alice configuring a second transport and removing the first one."""
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -64,18 +41,28 @@ def test_change_address(acf) -> None:
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.list_transports()[0]["addr"]
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
alice.delete_transport(old_alice_addr)
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr not in alice_vcard
|
||||
assert new_alice_addr in alice_vcard
|
||||
with pytest.raises(JsonRpcError):
|
||||
alice.delete_transport(new_alice_addr)
|
||||
alice.start_io()
|
||||
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
@@ -89,25 +76,6 @@ def test_change_address(acf) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_remove_transport_keep_messages(acf) -> None:
|
||||
"""Test that deleting current sending transport keeps queued messages."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
alice.stop_io()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.delete_transport(alice.list_transports()[0]["addr"])
|
||||
alice.start_io()
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert bob_msg.sender.get_snapshot().address == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acf, rpcdata) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
@@ -152,10 +120,6 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
if "scheduler is running" in ev.msg:
|
||||
return
|
||||
|
||||
def wait_transports(ac, n):
|
||||
while len(ac.list_transports()) != n:
|
||||
ac.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
@@ -163,13 +127,15 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
qr = acf.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
wait_transports(ac1_clone, 2)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
assert len(ac1.list_transports()) == 2
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
|
||||
ac1_clone.add_transport_from_qr(qr)
|
||||
wait_transports(ac1, 3)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
assert len(ac1.list_transports()) == 3
|
||||
assert len(ac1_clone.list_transports()) == 3
|
||||
|
||||
log.section("ac1 clone removes second transport")
|
||||
@@ -177,17 +143,21 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
addr3 = transport3["addr"]
|
||||
ac1_clone.delete_transport(transport2["addr"])
|
||||
|
||||
wait_transports(ac1, 2)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the sending transport")
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
[transport1, transport3] = ac1_clone.list_transports()
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
wait_transports(ac1_clone, 1)
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
@@ -209,7 +179,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
qr = acf.get_account_qr()
|
||||
|
||||
ac1.add_transport_from_qr(qr)
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
ac1_transports = ac1.list_transports()
|
||||
assert len(ac1_transports) == 2
|
||||
[transport1, transport2] = ac1_transports
|
||||
@@ -220,6 +189,9 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
@@ -262,8 +234,18 @@ def test_transport_limit(acf) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
# test that adding a transport after unpublishing one works again
|
||||
account.set_transport_unpublished(second_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
# UIs are not expected to delete transports directly,
|
||||
# but we still test that adding a transport
|
||||
# after deleting one instead of unpublishing works.
|
||||
account.delete_transport(third_addr)
|
||||
account.add_transport_from_qr(qr)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport_from_qr(qr)
|
||||
@@ -274,10 +256,11 @@ def test_message_info_imap_urls(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr = acf.get_account_qr()
|
||||
for _ in range(3):
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
# Wait for all transports to go IDLE after adding each one.
|
||||
alice.bring_online()
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
@@ -309,10 +292,18 @@ def test_message_info_imap_urls(acf) -> None:
|
||||
|
||||
def test_remove_primary_transport(acf, log) -> None:
|
||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
qr = acf.get_account_qr()
|
||||
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.bring_online()
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
alice.create_chat(bob)
|
||||
|
||||
log.section("Alice sets up second transport")
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
alice.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
@@ -320,7 +311,6 @@ def test_remove_primary_transport(acf, log) -> None:
|
||||
|
||||
log.section("Alice removes the primary relay")
|
||||
alice.delete_transport(transport1["addr"])
|
||||
assert alice.get_config("configured_addr") == transport2["addr"]
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
@@ -328,75 +318,4 @@ def test_remove_primary_transport(acf, log) -> None:
|
||||
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg2.text == "Hello again!"
|
||||
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
|
||||
assert msg2.chat == alice_chat
|
||||
|
||||
|
||||
def test_qr_works_after_removing_primary_transport(acf, log) -> None:
|
||||
log.section("Alice setups an account and adds two additional relays")
|
||||
alice = acf.new_configured_account()
|
||||
relay_qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(relay_qr)
|
||||
alice.add_transport_from_qr(relay_qr)
|
||||
|
||||
first_addr = alice.list_transports()[0]["addr"]
|
||||
second_addr = alice.list_transports()[1]["addr"]
|
||||
third_addr = alice.list_transports()[2]["addr"]
|
||||
|
||||
log.section("Alice creates a QR code")
|
||||
chat_qr = alice.get_qr_code()
|
||||
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
|
||||
assert f"&a={first_addr}" in chat_qr_unquoted
|
||||
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
|
||||
|
||||
log.section("Alice removes first and second transport")
|
||||
alice.set_config("configured_addr", third_addr)
|
||||
alice.delete_transport(first_addr)
|
||||
alice.delete_transport(second_addr)
|
||||
|
||||
log.section("Bob scans the QR code, which still works")
|
||||
alice.bring_online()
|
||||
bob = acf.get_online_account()
|
||||
bob.secure_join(chat_qr)
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
|
||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
assert alice.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
alice.stop_io()
|
||||
bob_chat.send_text("hello")
|
||||
imap1 = direct_imap(alice, transport1["addr"], transport1["password"])
|
||||
wait_for_imap_message(direct_imap(alice, transport2["addr"], transport2["password"]))
|
||||
wait_for_imap_message(imap1)
|
||||
|
||||
# Leave the message on the second transport only.
|
||||
imap1.delete("1:*")
|
||||
|
||||
dc.background_fetch(300)
|
||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
||||
|
||||
|
||||
def test_background_fetch_no_duplicates(acf, direct_imap, dc):
|
||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
||||
|
||||
alice.stop_io()
|
||||
bob_chat.send_text("hello")
|
||||
for transport in alice.list_transports():
|
||||
wait_for_imap_message(direct_imap(alice, transport["addr"], transport["password"]))
|
||||
|
||||
dc.background_fetch(300)
|
||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
||||
|
||||
|
||||
def test_multitransport_mdn(acf):
|
||||
"""Test sending an MDN right after configuring two transports."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice.create_chat(bob)
|
||||
bob_msg = bob.create_chat(alice).send_text("Hello!")
|
||||
alice.wait_for_incoming_msg().mark_seen()
|
||||
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
|
||||
assert msg2.chat == alice.create_chat(bob)
|
||||
|
||||
@@ -4,9 +4,10 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client.const import ChatType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_qr_setup_contact(acf) -> None:
|
||||
def test_qr_setup_contact(acf, tmp_path) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
@@ -14,15 +15,33 @@ def test_qr_setup_contact(acf) -> None:
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Test that if Bob imports a key,
|
||||
# backwards verification is not lost
|
||||
# because default key is not changed.
|
||||
logging.info("Bob 2 is created")
|
||||
bob2 = acf.new_configured_account()
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob tries to import a key")
|
||||
# Importing a second key is not allowed.
|
||||
with pytest.raises(JsonRpcError):
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "1"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acf) -> None:
|
||||
@@ -62,24 +81,26 @@ def test_qr_securejoin(acf):
|
||||
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes the securejoin protocol on the second device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.create_contact(bob)
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.e2ee_avail
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins the group via alice2")
|
||||
@@ -130,9 +151,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
assert snapshot2.chat_id == chat.id
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
# Check that the chat partner's key is known.
|
||||
# Check that the chat partner is verified.
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
assert contact_snapshot.e2ee_avail
|
||||
assert contact_snapshot.is_verified
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
chat_msgs = chat.get_messages()
|
||||
@@ -329,8 +350,8 @@ def test_setup_contact_resetup(acf) -> None:
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_group_member_added_recovery(acf) -> None:
|
||||
"""Tests group recovery after a member resets its key."""
|
||||
def test_verified_group_member_added_recovery(acf) -> None:
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates a group")
|
||||
@@ -341,7 +362,11 @@ def test_group_member_added_recovery(acf) -> None:
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("ac3 joins the group")
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
@@ -351,7 +376,7 @@ def test_group_member_added_recovery(acf) -> None:
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acf.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 scans ac3's QR code again")
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
@@ -391,6 +416,14 @@ def test_group_member_added_recovery(acf) -> None:
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
"""Regression test for
|
||||
@@ -398,13 +431,13 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: set up contact with ac2")
|
||||
logging.info("ac3: verify with ac2")
|
||||
qr_code = ac2.get_qr_code()
|
||||
ac3.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_inviter_success()
|
||||
|
||||
# in order for ac2 to have pending bobstate with a group
|
||||
# we first create a fully joined group, and then start
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
logging.info("ac1: create a group that ac2 fully joins")
|
||||
@@ -413,7 +446,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in the chat
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
@@ -426,11 +459,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
|
||||
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
|
||||
assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
|
||||
logging.info("ac3: create a group VG with ac2")
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
@@ -453,7 +486,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
|
||||
def test_qr_new_group_unblocked(acf):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a group QR code created by ac1.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked single chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
@@ -480,13 +513,13 @@ def test_qr_new_group_unblocked(acf):
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||
def test_aeap_flow(acf):
|
||||
def test_aeap_flow_verified(acf):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
|
||||
addr, password = acf.get_credentials()
|
||||
|
||||
logging.info("ac1: create group QR, ac2 scans and joins")
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello")
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
@@ -522,15 +555,65 @@ def test_aeap_flow(acf):
|
||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_gossip_verification(acf) -> None:
|
||||
alice, bob, carol = acf.get_online_accounts(3)
|
||||
|
||||
# Bob verifies Alice.
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Bob verifies Carol.
|
||||
qr_code = carol.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||
|
||||
logging.info("Bob creates an Autocrypt group")
|
||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||
bob_group_chat.add_contact(bob_contact_alice)
|
||||
bob_group_chat.add_contact(bob_contact_carol)
|
||||
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||
|
||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Autocrypt group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Group propagates verification using Autocrypt-Gossip header.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
logging.info("Bob creates a Securejoin group")
|
||||
bob_group_chat = bob.create_group("Securejoin Group")
|
||||
bob_group_chat.add_contact(bob_contact_alice)
|
||||
bob_group_chat.add_contact(bob_contact_carol)
|
||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||
|
||||
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Securejoin group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Securejoin propagates verification.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
"""
|
||||
Regression test for a bug that prevented joining a group with a QR code
|
||||
if the group already contains a contact with the same address as the inviter,
|
||||
but different key fingerprint while a securejoin with that contact is still pending.
|
||||
Regression test for a bug that prevented joining verified group with a QR code
|
||||
if the group is already created and contains
|
||||
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
|
||||
"""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
|
||||
# ac3 creates a group with ac1.
|
||||
# ac3 creates protected group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
@@ -543,24 +626,28 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 sets up contact with ac1
|
||||
# ac2 verifies ac1
|
||||
qr_code = ac1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 is verified for ac2.
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert ac2_contact_ac1.get_snapshot().e2ee_avail
|
||||
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 resetups the account.
|
||||
ac1 = acf.resetup_account(ac1)
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
# Scanning a QR code creates a group with the inviter, here ac1.
|
||||
# Normally the securejoin protocol
|
||||
# would complete and "Member added" would arrive,
|
||||
# but ac1 is offline so it never finishes.
|
||||
# Scanning a QR code results in creating an unprotected group with an inviter.
|
||||
# In this case inviter is ac1 which has an inconsistent key state.
|
||||
# Normally inviter becomes verified as a result of Securejoin protocol
|
||||
# and then the group chat becomes verified when "Member added" is received,
|
||||
# but in this case ac1 is offline and this Securejoin process will never finish.
|
||||
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||
ac2.secure_join(ac1_qr_code)
|
||||
|
||||
@@ -577,13 +664,16 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
ac2_chat = snapshot.chat
|
||||
assert len(ac2_chat.get_contacts()) == 3
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
|
||||
def test_withdraw_securejoin_qr(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins the group")
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
|
||||
@@ -421,6 +421,7 @@ def test_dont_move_sync_msgs(acf, direct_imap):
|
||||
addr, password = acf.get_credentials()
|
||||
ac1 = acf.get_unconfigured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.set_config("fix_is_chatmail", "1")
|
||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac1.start_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
@@ -745,11 +746,6 @@ def test_early_failure(tmp_path) -> None:
|
||||
with pytest.raises(JsonRpcError, match="invalid_dir"):
|
||||
rpc.start()
|
||||
|
||||
# Requests issued after the server exited must fail immediately
|
||||
# instead of waiting forever for the finished reader loop.
|
||||
with pytest.raises(JsonRpcError, match="RPC server closed"):
|
||||
rpc.get_system_info()
|
||||
|
||||
|
||||
def test_mdn_doesnt_break_autocrypt(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
@@ -1353,22 +1349,6 @@ def test_background_fetch(acf, dc):
|
||||
break
|
||||
|
||||
|
||||
def test_background_fetch_does_not_wait_for_sending(dc, acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
|
||||
alice.stop_io()
|
||||
text = "x" * 200_000
|
||||
for _ in range(50):
|
||||
alice_chat_bob.send_text(text)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
alice.start_io()
|
||||
dc.background_fetch(50)
|
||||
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
|
||||
def test_message_exists(acf):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -1450,35 +1430,3 @@ def test_large_message(acf, rpcdata) -> None:
|
||||
assert msg.id == msgs_changed_event.msg_id
|
||||
snapshot = msg.get_snapshot()
|
||||
assert snapshot.text == "Hello World, this message is bigger than 5 bytes"
|
||||
|
||||
|
||||
def test_is_sending_finished(dc, acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
|
||||
assert dc.is_sending_finished()
|
||||
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
|
||||
assert dc.is_sending_finished()
|
||||
|
||||
alice.stop_io()
|
||||
bob.stop_io()
|
||||
|
||||
bob_chat_alice.send_text("Hello back!")
|
||||
alice_chat_bob.send_text("Hello again!")
|
||||
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
alice.start_io()
|
||||
alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
bob.start_io()
|
||||
bob.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
|
||||
assert dc.is_sending_finished()
|
||||
|
||||
@@ -80,42 +80,6 @@ def read_database_schema(dbfile):
|
||||
return ";\n".join(row[0] for row in rows)
|
||||
|
||||
|
||||
# Tables where every column carries a comment in docs/schema.sql.
|
||||
# Opt-in: document a table's columns, then add it here to lock it in.
|
||||
FULLY_DOCUMENTED_TABLES = {
|
||||
"contacts",
|
||||
"imap_markseen",
|
||||
"multi_device_sync",
|
||||
"transports",
|
||||
}
|
||||
|
||||
|
||||
def undocumented_columns(sql, tables):
|
||||
result = []
|
||||
table = None
|
||||
documented = False
|
||||
for line in sql.splitlines():
|
||||
code, _, comment = line.strip().partition("--")
|
||||
code = code.strip()
|
||||
if code.startswith("CREATE TABLE "):
|
||||
name = code.removeprefix("CREATE TABLE ").partition("(")[0].strip()
|
||||
table = name if name in tables else None
|
||||
assert not table or code.endswith("("), f"{code}: want one column per line"
|
||||
elif code.startswith(")"):
|
||||
table = None
|
||||
elif table and code and not re.match(r"(UNIQUE|PRIMARY|FOREIGN|CHECK)\b", code):
|
||||
column = re.match(r"(\w+) (?!INTEGER PRIMARY KEY)", code)
|
||||
if column and not documented and not comment:
|
||||
result.append(f"{table}.{column.group(1)}")
|
||||
documented = bool(comment) and not code
|
||||
return result
|
||||
|
||||
|
||||
def test_documented_tables_stay_documented():
|
||||
missing = undocumented_columns(DOC_PATH.read_text(), FULLY_DOCUMENTED_TABLES)
|
||||
assert not missing, "columns without a comment in docs/schema.sql:\n" + "\n".join(missing)
|
||||
|
||||
|
||||
def test_documented_schema_matches_database(acf):
|
||||
account = acf.get_unconfigured_account()
|
||||
real = parse_schema(read_database_schema(account.get_info()["database_dir"]))
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_webxdc(acf, rpcdata) -> None:
|
||||
"isAppSender": False,
|
||||
"isBroadcast": False,
|
||||
"sendUpdateInterval": 1000,
|
||||
"sendUpdateMaxSize": 2**20 * (30 - 1) * 3 // 4,
|
||||
"sendUpdateMaxSize": 18874368,
|
||||
}
|
||||
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
@@ -11,7 +11,7 @@ Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`
|
||||
To install from source run:
|
||||
|
||||
```sh
|
||||
cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
```
|
||||
|
||||
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.60.0-dev"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
|
||||
|
||||
if (is_local) {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
|
||||
} else {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ENV_VAR_NAME } from "./const.js";
|
||||
|
||||
const cargoInstallCommand =
|
||||
"cargo install --locked --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||
|
||||
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
||||
return `deltachat-rpc-server not found:
|
||||
|
||||
@@ -72,7 +72,8 @@ CREATE TABLE contacts (
|
||||
-- empty string for "address-contacts".
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Unused. Was the ID of the contact that introduced this contact's key.
|
||||
-- ID of the contact that has "introduced" us to this contact
|
||||
-- by sharing the key with a verified attribute or in a "verified" chat.
|
||||
verifier INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
|
||||
@@ -130,7 +131,9 @@ CREATE TABLE chats (
|
||||
-- 0 means the timer is disabled.
|
||||
ephemeral_timer INTEGER,
|
||||
|
||||
-- Unused. Was 1 for protected chats.
|
||||
-- Deprecated, but still used to send Chat-Verified headers
|
||||
-- for existing protected chats.
|
||||
-- All new chats are created as "not protected".
|
||||
protected INTEGER DEFAULT 0,
|
||||
|
||||
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
|
||||
@@ -397,51 +400,14 @@ CREATE TABLE bobstate (
|
||||
chat_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE smtp2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
display_name TEXT NOT NULL, -- Display name to put into the From field.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
|
||||
-- Unencrypted payload with some headers.
|
||||
mime BLOB NOT NULL,
|
||||
|
||||
-- True if Autocrypt header should be added before sending.
|
||||
should_attach_pubkey INTEGER NOT NULL,
|
||||
|
||||
-- True if OpenPGP-encrypted message may use compression.
|
||||
should_compress INTEGER NOT NULL,
|
||||
|
||||
-- True if encrypted message should be signed.
|
||||
should_sign INTEGER NOT NULL,
|
||||
|
||||
-- ID of the message in `msgs` table
|
||||
msg_id INTEGER NOT NULL,
|
||||
|
||||
-- Space-separated recipient addresses.
|
||||
recipients TEXT NOT NULL,
|
||||
|
||||
-- Space-separated addresses the message was sent to.
|
||||
sent_to TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- If true, copy should be sent to self in addition to the recipient list.
|
||||
--
|
||||
-- For encrypted messages copy is sent to all addresses.
|
||||
-- For unencrypted messages, copy is sent to the From address only.
|
||||
bcc_self INTEGER NOT NULL,
|
||||
|
||||
-- True if the message is encrypted.
|
||||
-- If true, at most one of the shared_secret or encryption_fingerprints should be non-empty.
|
||||
-- If false, both must be empty.
|
||||
is_encrypted INTEGER NOT NULL,
|
||||
|
||||
-- Shared secret if the message is to be encrypted symmetrically.
|
||||
shared_secret TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Space-separated fingerprints of the keys the message should be encrypted to.
|
||||
encryption_fingerprints TEXT NOT NULL DEFAULT '',
|
||||
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
) STRICT;
|
||||
CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
|
||||
CREATE TABLE smtp_mdns (
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
|
||||
@@ -655,15 +621,15 @@ CREATE TABLE transports (
|
||||
-- this is ensured during configuration.
|
||||
configured_param TEXT NOT NULL,
|
||||
|
||||
-- Bumping this forces re-signing the key:
|
||||
-- the relay-list signature is created at MAX(add_timestamp, remove_timestamp)
|
||||
-- over this table and `removed_transports`, and contacts keep the newest one.
|
||||
add_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Unused since migration 165, which removed unpublished transports.
|
||||
-- True if the transport address is published
|
||||
-- by sending it in the public key signature.
|
||||
is_published INTEGER DEFAULT 1 NOT NULL,
|
||||
|
||||
-- Time when the transport was last used to receive a message.
|
||||
-- Used to remove the least recently used transport
|
||||
-- when a new transport is added and there are too many relays already.
|
||||
last_rcvd_timestamp INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(addr)
|
||||
);
|
||||
@@ -727,13 +693,13 @@ CREATE TABLE stats_securejoin_uipaths(
|
||||
) STRICT;
|
||||
CREATE TABLE stats_securejoin_invites(
|
||||
already_existed INTEGER NOT NULL,
|
||||
already_verified INTEGER NOT NULL, -- unused, always 0
|
||||
already_verified INTEGER NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE stats_msgs(
|
||||
chattype INTEGER PRIMARY KEY,
|
||||
verified INTEGER NOT NULL DEFAULT 0, -- unused, always 0
|
||||
unverified_encrypted INTEGER NOT NULL DEFAULT 0, -- counts all encrypted messages
|
||||
verified INTEGER NOT NULL DEFAULT 0,
|
||||
unverified_encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
unencrypted INTEGER NOT NULL DEFAULT 0,
|
||||
only_to_self INTEGER NOT NULL DEFAULT 0,
|
||||
last_counted_msg_id INTEGER NOT NULL DEFAULT 0
|
||||
@@ -815,13 +781,3 @@ CREATE TABLE sending_domains(
|
||||
domain TEXT PRIMARY KEY,
|
||||
dkim_works INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- Replaced with smtp2.
|
||||
CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
|
||||
445
flake.nix
445
flake.nix
@@ -29,7 +29,11 @@
|
||||
rustc = fenixToolchain;
|
||||
};
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
version = manifest.version;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -47,7 +51,6 @@
|
||||
./deltachat-contact-tools
|
||||
./deltachat-ffi
|
||||
./deltachat-jsonrpc
|
||||
./deltachat-jsonrpc-bindings
|
||||
./deltachat-ratelimit
|
||||
./deltachat-repl
|
||||
./deltachat-rpc-client
|
||||
@@ -80,7 +83,7 @@
|
||||
naersk'.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
@@ -88,22 +91,221 @@
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
};
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
mkWin64RustPackage = pkgs.callPackage ./nix/win64-package.nix {
|
||||
inherit naersk system fenixPkgs version;
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin64.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkWin32RustPackage = packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions"
|
||||
"--with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
"-L"
|
||||
"native=${pkgsWin32.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
};
|
||||
|
||||
mkCrossRustPackage = arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi21-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
x86 = {
|
||||
cc = "i686-linux-android21-clang";
|
||||
rustTarget = "i686-linux-android";
|
||||
};
|
||||
x86_64 = {
|
||||
cc = "x86_64-linux-android21-clang";
|
||||
rustTarget = "x86_64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
mkWin32RustPackage = pkgs.callPackage ./nix/win32-package.nix {
|
||||
inherit naersk system fenixPkgs version;
|
||||
};
|
||||
mkAndroidRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
mkCrossRustPackage = pkgs.callPackage ./nix/cross-rust-package.nix {
|
||||
inherit nixpkgs arch2targets naersk fenixPkgs system rustSrc version;
|
||||
};
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
mkAndroidRustPackage = pkgs.callPackage ./nix/android-package.nix {
|
||||
inherit naersk fenixPkgs system rustSrc android version;
|
||||
};
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch:
|
||||
let
|
||||
@@ -113,7 +315,32 @@
|
||||
"deltachat-rpc-server-${arch}-android" = rpc-server;
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}-android-wheel" =
|
||||
mkWheel { inherit rpc-server; arch = "${arch}-android"; };
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-android-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
@@ -123,10 +350,34 @@
|
||||
{
|
||||
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}" = rpc-server;
|
||||
"deltachat-rpc-server-${arch}-wheel" = mkWheel { inherit rpc-server; arch = "${arch}"; };
|
||||
"deltachat-rpc-server-${arch}-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
|
||||
mkWheel = pkgs.callPackage ./nix/wheel.nix { inherit nix-filter version; root = ./.; };
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixpkgs-fmt;
|
||||
@@ -150,29 +401,116 @@
|
||||
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win64-wheel =
|
||||
mkWheel { rpc-server = deltachat-rpc-server-win64; arch = "win64"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win64-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win64
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
|
||||
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
|
||||
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
|
||||
deltachat-rpc-server-win32-wheel =
|
||||
mkWheel
|
||||
{ rpc-server = deltachat-rpc-server-win32; arch = "win32"; binaryName = "deltachat-rpc-server.exe"; };
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-win32-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
deltachat-rpc-server-win32
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
|
||||
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
# Run `nix build .#docs` to get C docs generated in `./result/`.
|
||||
docs = pkgs.callPackage ./nix/c-docs.nix { inherit version; };
|
||||
docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
};
|
||||
|
||||
libdeltachat = pkgs.callPackage ./nix/libdeltachat.nix {
|
||||
inherit fenixToolchain rustSrc cargoLock fenixPkgs version;
|
||||
};
|
||||
libdeltachat =
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
deltachat-rpc-client = pkgs.callPackage ./nix/deltachat-rpc-client.nix {
|
||||
inherit version;
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
inherit version;
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
format = "pyproject";
|
||||
buildInputs = [
|
||||
@@ -190,12 +528,51 @@
|
||||
pkgs.python3Packages.requests
|
||||
];
|
||||
};
|
||||
python-docs = pkgs.callPackage ./nix/python-docs.nix {
|
||||
inherit deltachat-python deltachat-rpc-client version;
|
||||
};
|
||||
python-docs =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx-rtd-theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = import ./nix/shell.nix { inherit nixpkgs fenix system; };
|
||||
devShells.default =
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version, rustSrc, android }:
|
||||
arch: packageName:
|
||||
let
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi21-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
x86 = {
|
||||
cc = "i686-linux-android21-clang";
|
||||
rustTarget = "i686-linux-android";
|
||||
};
|
||||
x86_64 = {
|
||||
cc = "x86_64-linux-android21-clang";
|
||||
rustTarget = "x86_64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{ pkgs, version }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [ pkgs.doxygen ];
|
||||
buildPhase = ''scripts/run-doxygen.sh'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{ pkgs, nixpkgs, arch2targets, naersk, fenixPkgs, system, rustSrc, version }:
|
||||
arch: packageName:
|
||||
let
|
||||
crossTarget = arch2targets."${arch}";
|
||||
pkgsCross = import nixpkgs {
|
||||
system = system;
|
||||
crossSystem.config = crossTarget;
|
||||
};
|
||||
rustTarget = pkgsCross.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetCc = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
in
|
||||
naersk-lib.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{ pkgs, version }:
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.imap-tools
|
||||
];
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{ pkgs, fenixToolchain, rustSrc, cargoLock, fenixPkgs, version }:
|
||||
let
|
||||
rustPlatform = (pkgs.makeRustPlatform {
|
||||
cargo = fenixToolchain;
|
||||
rustc = fenixToolchain;
|
||||
});
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
inherit version;
|
||||
src = rustSrc;
|
||||
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.cmake
|
||||
rustPlatform.cargoSetupHook
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
substituteInPlace $out/include/deltachat.h \
|
||||
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
|
||||
'';
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{ pkgs, version, deltachat-python, deltachat-rpc-client }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "docs";
|
||||
inherit version;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
buildInputs = [
|
||||
deltachat-python
|
||||
deltachat-rpc-client
|
||||
pkgs.python3Packages.breathe
|
||||
pkgs.python3Packages.sphinx-rtd-theme
|
||||
];
|
||||
nativeBuildInputs = [ pkgs.sphinx ];
|
||||
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{ nixpkgs, fenix, system }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
(python3.withPackages (pypkgs: [
|
||||
pypkgs.tox
|
||||
]))
|
||||
nodejs
|
||||
];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{ pkgs, nix-filter, version, root }:
|
||||
{ rpc-server, arch, binaryName ? "deltachat-rpc-server" }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-wheel";
|
||||
inherit version;
|
||||
src = nix-filter.lib {
|
||||
inherit root;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/${binaryName} tmp/${binaryName}
|
||||
python3 scripts/wheel-rpc-server.py ${arch} tmp/${binaryName}
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version }:
|
||||
packageName:
|
||||
let
|
||||
pkgsWin32 = pkgs.pkgsCross.mingw32;
|
||||
rustTarget = pkgsWin32.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
|
||||
# Get rid of MCF Gthread library.
|
||||
# See <https://github.com/NixOS/nixpkgs/issues/156343>
|
||||
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
|
||||
# for details.
|
||||
#
|
||||
# Use DWARF-2 instead of SJLJ for exception handling.
|
||||
winCC = pkgsWin32.buildPackages.wrapCC (
|
||||
(pkgsWin32.buildPackages.gcc-unwrapped.override
|
||||
({
|
||||
threadsCross = {
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions"
|
||||
"--with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
targetCc = "${winCC}/bin/${winCC.targetPrefix}cc";
|
||||
in
|
||||
naerskWin.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
pkgs.nasm # aws-lc-sys requires it
|
||||
];
|
||||
depsBuildBuild = [
|
||||
winCC
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin32.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CFLAGS_i686_pc_windows_gnu = "-I${pkgsWin32.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
"-L"
|
||||
"native=${pkgsWin32.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{ pkgs, naersk, fenixPkgs, system, version }:
|
||||
packageName:
|
||||
let
|
||||
pkgsWin64 = pkgs.pkgsCross.mingwW64;
|
||||
rustTarget = pkgsWin64.stdenv.hostPlatform.rust.rustcTarget;
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naerskWin = pkgs.callPackage naersk {
|
||||
cargo = toolchainWin;
|
||||
rustc = toolchainWin;
|
||||
};
|
||||
targetCc = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
|
||||
in
|
||||
naerskWin.buildPackage {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
inherit version;
|
||||
strictDeps = true;
|
||||
src = pkgs.lib.cleanSource ../.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
depsBuildBuild = [
|
||||
pkgsWin64.stdenv.cc
|
||||
];
|
||||
buildInputs = [
|
||||
pkgsWin64.windows.pthreads
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CFLAGS_x86_64_pc_windows_gnu = "-I${pkgsWin64.windows.pthreads}/include";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${targetCc}"
|
||||
"-L"
|
||||
"native=${pkgsWin64.windows.pthreads}/lib"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
}
|
||||
@@ -17,7 +17,7 @@ Install ``deltachat-rpc-server``
|
||||
To get ``deltachat-rpc-server`` binary you have three options:
|
||||
|
||||
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --locked --git https://github.com/chatmail/core/ deltachat-rpc-server``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``.
|
||||
|
||||
Check that ``deltachat-rpc-server`` is installed and can run::
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.60.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -259,15 +259,6 @@ class Chat:
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def _reload_sent_msg(self, msg: Message, sent_id: int) -> Message:
|
||||
"""Helper to reload just sent message"""
|
||||
sent_msg = Message.from_db(self.account, sent_id)
|
||||
if sent_msg is None:
|
||||
raise ValueError("cannot load just sent message from the database")
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = sent_msg._dc_msg
|
||||
return msg
|
||||
|
||||
def send_msg(self, msg: Message) -> Message:
|
||||
"""send a message by using a ready Message object.
|
||||
|
||||
@@ -283,25 +274,12 @@ class Chat:
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
return self._reload_sent_msg(msg, sent_id)
|
||||
|
||||
def send_msg_sync(self, msg: Message) -> Message:
|
||||
"""Send a message synchronously.
|
||||
This bypasses the IO scheduler and creates its own SMTP connection.
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
previously returned by
|
||||
e.g. :meth:`deltachat.message.Message.new_empty`.
|
||||
:raises ValueError: if message can not be sent.
|
||||
|
||||
:returns: a :class:`deltachat.message.Message` instance as
|
||||
sent out. This is the same object as was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
sent_id = lib.dc_send_msg_sync(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
return self._reload_sent_msg(msg, sent_id)
|
||||
# modify message in place to avoid bad state for the caller
|
||||
sent_msg = Message.from_db(self.account, sent_id)
|
||||
if sent_msg is None:
|
||||
raise ValueError("cannot load just sent message from the database")
|
||||
msg._dc_msg = sent_msg._dc_msg
|
||||
return msg
|
||||
|
||||
def send_text(self, text):
|
||||
"""send a text message and return the resulting Message instance.
|
||||
|
||||
@@ -71,6 +71,17 @@ class Contact:
|
||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self) -> bool:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
|
||||
@@ -104,11 +104,11 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch(mark_seen=False))
|
||||
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), mark_seen=False)]
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
@@ -183,7 +183,7 @@ class DirectImap:
|
||||
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)), mark_seen=False)]
|
||||
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]
|
||||
@@ -193,6 +193,9 @@ 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]:
|
||||
|
||||
@@ -11,9 +11,6 @@ import random
|
||||
from queue import Queue
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from .capi import lib
|
||||
from .cutil import as_dc_charpointer
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Source
|
||||
|
||||
@@ -369,7 +366,6 @@ class ACFactory:
|
||||
ac.open(passphrase)
|
||||
acname = ac._logid
|
||||
addr = f"{acname}@offline.org"
|
||||
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
|
||||
ac.update_config(
|
||||
{
|
||||
"configured_addr": addr,
|
||||
@@ -378,7 +374,6 @@ class ACFactory:
|
||||
)
|
||||
self._preconfigure_key(ac)
|
||||
self._acsetup.init_logging(ac)
|
||||
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
|
||||
return ac
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs) -> Account:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import time
|
||||
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
@@ -61,9 +63,9 @@ class TestGroupStressTests:
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_qr_group_join_and_chatting(acfactory, lp):
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
@@ -84,11 +86,15 @@ def test_qr_group_join_and_chatting(acfactory, lp):
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
|
||||
lp.sec("ac2: read message and check that it is encrypted")
|
||||
lp.sec("ac2: read message and check that it's a verified chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
@@ -103,13 +109,23 @@ def test_qr_group_join_and_chatting(acfactory, lp):
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac1: add ac3 to the group")
|
||||
lp.sec("ac1: add ac3 to verified group")
|
||||
chat1.add_contact(ac3)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert not msg.error
|
||||
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
for ac2_contact in chat2.get_contacts():
|
||||
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
|
||||
continue
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# System message about the added member.
|
||||
@@ -179,10 +195,10 @@ def test_ephemeral_timer(acfactory, lp):
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
- Alice creates a group and sends a QR invitation to Bob.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group and sends a message there. Alice sees it.
|
||||
- Alice's second devices goes online, but doesn't see Bob in the group.
|
||||
"""
|
||||
@@ -199,7 +215,7 @@ def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
|
||||
ac1_offl.import_self_keys(str(dir))
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
@@ -226,12 +242,12 @@ def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
|
||||
def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a group and sends a QR invitation to Bob.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the verified group.
|
||||
- The "member added" message is not a system message but a plain text message.
|
||||
- Bob's second device doesn't display the Alice's avatar (bug #5354).
|
||||
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
|
||||
@@ -253,7 +269,7 @@ def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
avatar_path = data.get_path("d.png")
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
|
||||
@@ -291,32 +291,6 @@ def test_forward_own_message(acfactory, lp):
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
|
||||
def test_send_msg_sync(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
# Send some message from ac1
|
||||
# so we are testing not the first message
|
||||
# being sent synchronously.
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat1.send_text("message")
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message"
|
||||
|
||||
# Stop I/O and send message synchronously.
|
||||
ac1.stop_io()
|
||||
msg1 = Message.new_empty(ac1, "text")
|
||||
msg1.set_text("message1")
|
||||
chat1.send_msg_sync(msg1)
|
||||
msg1.is_out_delivered()
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
|
||||
|
||||
def test_resend_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -796,8 +770,9 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Tests joining a group via QR code
|
||||
when the database already contains a contact with a different email address capitalization.
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
@@ -821,7 +796,13 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
# ac1 should see both ac3 and ac2 as verified.
|
||||
assert len(ac1_chat.get_contacts()) == 3
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# the verification of ac2 is not gossiped here:
|
||||
for contact in ac1_chat.get_contacts():
|
||||
is_ac2 = contact.addr == ac2.get_config("addr")
|
||||
assert contact.is_verified() != is_ac2
|
||||
|
||||
|
||||
def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
@@ -1152,9 +1133,8 @@ def test_configure_error_msgs_wrong_pw(acfactory):
|
||||
print(f"Configuration progress: {ev.data1}")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
# Password is wrong so the error should be about authentication
|
||||
# and not e.g. connection failure.
|
||||
assert "Authentication" in ev.data2
|
||||
# Password is wrong so it definitely has to say something about "password"
|
||||
assert "password" in ev.data2
|
||||
|
||||
ac1.stop_io()
|
||||
ac1.set_config("mail_pw", "abc") # Wrong mail pw
|
||||
@@ -1164,7 +1144,10 @@ def test_configure_error_msgs_wrong_pw(acfactory):
|
||||
print(f"Configuration progress: {ev.data1}")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
assert "Authentication" in ev.data2
|
||||
assert "password" in ev.data2
|
||||
# Account will continue to work with the old password, so if it becomes wrong, a notification
|
||||
# must be shown.
|
||||
assert ac1.get_config("notify_about_wrong_pw") == "1"
|
||||
|
||||
|
||||
def test_configure_error_msgs_invalid_server(acfactory):
|
||||
|
||||
@@ -115,6 +115,7 @@ class TestOfflineContact:
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_get_blocked(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
@@ -132,7 +133,7 @@ class TestOfflineContact:
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("configured_addr"))
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
@@ -223,7 +224,7 @@ class TestOfflineChat:
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("configured_addr")
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
@@ -456,7 +457,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -552,7 +553,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -604,7 +605,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -621,7 +622,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-09-11
|
||||
2026-08-14
|
||||
@@ -6,4 +6,4 @@
|
||||
#
|
||||
# To automatically fix warnings, run
|
||||
# scripts/clippy.sh --fix --allow-dirty
|
||||
cargo clippy --locked --workspace --all-targets --all-features "$@" -- -D warnings
|
||||
cargo clippy --workspace --all-targets --all-features "$@" -- -D warnings
|
||||
|
||||
@@ -67,14 +67,13 @@ def main():
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = [
|
||||
"deltachat-jsonrpc-bindings/typescript/package.json",
|
||||
"deltachat-jsonrpc/typescript/package.json",
|
||||
"deltachat-rpc-server/npm-package/package.json",
|
||||
]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-jsonrpc-bindings/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
"python/pyproject.toml",
|
||||
|
||||
@@ -20,16 +20,6 @@ Description-Content-Type: text/markdown
|
||||
"""
|
||||
|
||||
|
||||
def wheel_contents(tag):
|
||||
"""Render the WHEEL metadata for a filename tag."""
|
||||
interpreter, abi, platforms = tag.split("-")
|
||||
lines = ["Wheel-Version: 1.0", "Root-Is-Purelib: false"]
|
||||
lines += [
|
||||
f"Tag: {interpreter}-{abi}-{platform}" for platform in platforms.split(".")
|
||||
]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def build_wheel(version, binary, tag, windows=False):
|
||||
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
|
||||
|
||||
@@ -70,7 +60,7 @@ def main():
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/WHEEL",
|
||||
wheel_contents(tag),
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: {tag}",
|
||||
)
|
||||
wheel.writestr(
|
||||
f"deltachat_rpc_server-{version}.dist-info/entry_points.txt",
|
||||
@@ -97,11 +87,7 @@ arch2tags = {
|
||||
def main():
|
||||
with Path("Cargo.toml").open("rb") as fp:
|
||||
cargo_manifest = tomllib.load(fp)
|
||||
|
||||
# Cargo's SEMVER "-dev" suffix is not a PEP 440 version,
|
||||
# so translate to make wheel tooling and metadata writing happy.
|
||||
version = cargo_manifest["package"]["version"].replace("-dev", ".dev0")
|
||||
|
||||
version = cargo_manifest["package"]["version"]
|
||||
arch = sys.argv[1]
|
||||
executable = sys.argv[2]
|
||||
tags = arch2tags[arch]
|
||||
|
||||
18
spec.md
18
spec.md
@@ -596,6 +596,24 @@ and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Verifications
|
||||
|
||||
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
|
||||
and corresponding contacts
|
||||
are considered "verified".
|
||||
|
||||
As an extension to `Autocrypt-Gossip` header,
|
||||
chatmail clients can add `_verified=1` attribute
|
||||
(underscore marks the attribute as non-critical)
|
||||
to indicate that they have the gossiped key
|
||||
and the corresponding contact marked as verified.
|
||||
|
||||
When receiving such `Autocrypt-Gossip` header
|
||||
in a message signed by a verified key,
|
||||
chatmail clients mark the gossiped key
|
||||
as indirectly verified.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::smtp;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
@@ -443,12 +442,7 @@ impl Accounts {
|
||||
interrupt_receiver: Option<Receiver<()>>,
|
||||
) {
|
||||
let Some(interrupt_receiver) = interrupt_receiver else {
|
||||
// Another background fetch is already running.
|
||||
// Emit the event anyway so that a caller waiting for it does not hang.
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
// Nothing to do if we got no interrupt receiver.
|
||||
return;
|
||||
};
|
||||
if let Err(_err) = tokio::time::timeout(
|
||||
@@ -482,15 +476,9 @@ impl Accounts {
|
||||
/// return immediately even before the timeout expiration
|
||||
/// or finishing fetching.
|
||||
///
|
||||
/// Pending outgoing messages are not waited for and not triggered.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// If another background fetch is already running,
|
||||
/// nothing is fetched and the event is emitted immediately.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it only safely refers to your call if no concurrent background fetch is happening.
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
@@ -521,20 +509,6 @@ impl Accounts {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if there are no pending messages for sending.
|
||||
///
|
||||
/// This is intended to be used by UIs to request not moving the app to background
|
||||
/// when there are messages left in the queue.
|
||||
pub async fn is_sending_finished(&self) -> Result<bool> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
for account in accounts {
|
||||
if !smtp::queue::is_empty(&account).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Interrupts ongoing background_fetch() call,
|
||||
/// making it return early.
|
||||
///
|
||||
@@ -1250,29 +1224,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_background_fetch_emits_done_when_already_running() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(dir.path().join("accounts"), writable).await?;
|
||||
let event_emitter = accounts.get_event_emitter();
|
||||
|
||||
let timeout = std::time::Duration::from_secs(3);
|
||||
let first = accounts.background_fetch(timeout);
|
||||
let second = accounts.background_fetch(timeout);
|
||||
tokio::join!(first, second);
|
||||
|
||||
let mut done = 0;
|
||||
while let Ok(event) = event_emitter.try_recv() {
|
||||
if matches!(event.typ, EventType::AccountsBackgroundFetchDone) {
|
||||
done += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(done, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
|
||||
@@ -43,6 +43,13 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
|
||||
/// Whether `_verified` attribute is present.
|
||||
///
|
||||
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
/// header that is used to tell that the sender
|
||||
/// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -51,6 +58,12 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
// TODO After we reset all existing verifications,
|
||||
// we want to start sending the _verified attribute
|
||||
// if self.verified {
|
||||
// write!(fmt, " _verified=1;")?;
|
||||
// }
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
// (which may insert a linebreak before every whitespace)
|
||||
@@ -101,6 +114,8 @@ impl Aheader {
|
||||
.and_then(|raw| EncryptPreference::new(&raw).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
@@ -111,6 +126,7 @@ impl Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -129,6 +145,7 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -226,6 +243,7 @@ mod tests {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
@@ -241,6 +259,7 @@ mod tests {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
@@ -254,9 +273,24 @@ mod tests {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
// We don't send the _verified header yet:
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: true
|
||||
}
|
||||
)
|
||||
.contains("_verified")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,40 +388,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// some systems may not get the update URL from the relays, but use a specific one.
|
||||
// this is esp. useful when the app binary is not signed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_app_versions_no_url() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
let account_id1 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltawin",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "windows-3.11",
|
||||
"versionInteger": 300,
|
||||
"versionString": "3.00.000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id1, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "deltawin", "windows-3.11")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(version.version_integer, 300);
|
||||
assert_eq!(version.version_string, "3.00.000");
|
||||
assert_eq!(version.download_url, ""); // no url given results in default empty string
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
//! # Automatic relay handling (experimental, still in development)
|
||||
//!
|
||||
//! Chatmail relays create an account on first login,
|
||||
//! so a profile can add further transports on its own without user interaction.
|
||||
//! Candidate hosts come from the `relay_candidates` table,
|
||||
//! which migrations seed with a list of known chatmail relays.
|
||||
//!
|
||||
//! Status of implementation:
|
||||
//! Additions are attempted right before going into IMAP IDLE,
|
||||
//! i.e. only while connected and with nothing more important to do,
|
||||
//! and only if a UI opted in via [`Config::Autorelay`].
|
||||
//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports,
|
||||
//! [`Config::AutorelayFinished`] is set and nothing is ever added again,
|
||||
//! so deleting a transport later does not pull in a replacement.
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -61,7 +46,9 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
// Housekeeping or automatic relay management is already running in another thread, do nothing.
|
||||
return Ok(false);
|
||||
};
|
||||
let last_timestamp = context.get_config_i64(Config::LastAutorelay).await?;
|
||||
let last_timestamp = context
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
if last_timestamp > now {
|
||||
warn!(
|
||||
context,
|
||||
@@ -70,16 +57,22 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
} else if last_timestamp > now.saturating_sub(AUTOMATIC_ADDITION_DEBOUNCE_SECONDS) {
|
||||
return Ok(false);
|
||||
}
|
||||
if !context.get_config_bool(Config::Autorelay).await? {
|
||||
if !context
|
||||
.get_config_bool(Config::AutomaticRelayManagement)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if context.get_config_bool(Config::AutorelayFinished).await? {
|
||||
if context
|
||||
.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
// Set the config at the beginning to avoid endless loops.
|
||||
// Race conditions are not a concern because we locked the mutex.
|
||||
context
|
||||
.set_config_internal(Config::LastAutorelay, Some(&now.to_string()))
|
||||
.set_config_internal(Config::LastAutomaticRelayManagement, Some(&now.to_string()))
|
||||
.await?;
|
||||
|
||||
let mut relay_added = false;
|
||||
@@ -87,7 +80,10 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
for _ in 0..NUM_TRANSPORTS_TARGET {
|
||||
if context.count_transports().await? >= NUM_TRANSPORTS_TARGET {
|
||||
context
|
||||
.set_config_internal(Config::AutorelayFinished, config::from_bool(true))
|
||||
.set_config_internal(
|
||||
Config::AutomaticRelayManagementFinished,
|
||||
config::from_bool(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(relay_added);
|
||||
@@ -118,8 +114,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
(now, host),
|
||||
)
|
||||
.await?;
|
||||
let mark_as_autorelay = true;
|
||||
let param = login_param_from_host(host, mark_as_autorelay);
|
||||
let param = login_param_from_host(host);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
@@ -159,20 +154,13 @@ async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
||||
pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + host;
|
||||
let addr = addr_normalize(&addr);
|
||||
|
||||
// `mark_as_autorelay` is a temporary precaution hack
|
||||
// while introducing onboarding on multiple community relays from a list:
|
||||
// though relay operators were asked to get on that list, unexpected things can happen,
|
||||
// and they want to return to allow only manual onboarding.
|
||||
// this is possible by failing on `password_len == 23`.
|
||||
|
||||
// 22 * log2(26 * 2 + 10) = 130 bits of entropy
|
||||
let password = Alphanumeric.sample_string(rng, if mark_as_autorelay { 23 } else { 22 });
|
||||
let password = Alphanumeric.sample_string(rng, 22);
|
||||
|
||||
EnteredLoginParam {
|
||||
addr,
|
||||
@@ -187,4 +175,4 @@ pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> Ente
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod autorelay_tests;
|
||||
mod automatic_relay_management_tests;
|
||||
@@ -73,9 +73,12 @@ async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_autorelay_does_nothing(t: &TestContext) {
|
||||
async fn assert_automatic_relay_management_does_nothing(t: &TestContext) {
|
||||
let transports_before = t.count_transports().await.unwrap();
|
||||
let config_before = t.get_config_i64(Config::LastAutorelay).await.unwrap();
|
||||
let config_before = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let skip_network = false; // No need to skip network, nothing is supposed to happen
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network)
|
||||
@@ -83,7 +86,10 @@ async fn assert_autorelay_does_nothing(t: &TestContext) {
|
||||
.unwrap();
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
let config_after = t.get_config_i64(Config::LastAutorelay).await.unwrap();
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await
|
||||
.unwrap();
|
||||
let transports_after = t.count_transports().await.unwrap();
|
||||
|
||||
assert_eq!(config_after, config_before);
|
||||
@@ -99,7 +105,7 @@ async fn test_maybe_add_additional_relays_mutex_held() -> Result<()> {
|
||||
// already running housekeeping or relay management.
|
||||
let _lock = t.background_task_mutex.lock().await;
|
||||
|
||||
assert_autorelay_does_nothing(t).await;
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -111,10 +117,13 @@ async fn test_maybe_add_additional_relays_debounce() -> Result<()> {
|
||||
let some_seconds_ago = time() - 10;
|
||||
|
||||
// Pretend automatic relay management just ran.
|
||||
t.set_config_internal(Config::LastAutorelay, Some(&some_seconds_ago.to_string()))
|
||||
.await?;
|
||||
t.set_config_internal(
|
||||
Config::LastAutomaticRelayManagement,
|
||||
Some(&some_seconds_ago.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_autorelay_does_nothing(t).await;
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -123,7 +132,7 @@ async fn test_maybe_add_additional_relays_debounce() -> Result<()> {
|
||||
async fn test_maybe_add_additional_relays_disabled() {
|
||||
// By default, automatic relay management is disabled:
|
||||
let t = &TestContext::new_alice().await;
|
||||
assert_autorelay_does_nothing(t).await;
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
}
|
||||
|
||||
/// Runs maybe_add_additional_relays_inner(), then deletes one of the transports.
|
||||
@@ -139,7 +148,8 @@ async fn test_maybe_add_additional_relays_does_nothing_after_finishing_once() ->
|
||||
assert!(relay_added);
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
t.delete_transport(&transports.last().unwrap().addr).await?;
|
||||
t.delete_transport(&transports.last().unwrap().param.addr)
|
||||
.await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(
|
||||
AUTOMATIC_ADDITION_DEBOUNCE_SECONDS as u64 + 1,
|
||||
@@ -148,8 +158,11 @@ async fn test_maybe_add_additional_relays_does_nothing_after_finishing_once() ->
|
||||
let transports_count = t.count_transports().await?;
|
||||
assert_eq!(transports_count, NUM_TRANSPORTS_TARGET - 1);
|
||||
|
||||
assert!(t.get_config_bool(Config::AutorelayFinished).await?);
|
||||
assert_autorelay_does_nothing(t).await;
|
||||
assert!(
|
||||
t.get_config_bool(Config::AutomaticRelayManagementFinished)
|
||||
.await?
|
||||
);
|
||||
assert_automatic_relay_management_does_nothing(t).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,7 +187,9 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t.get_config_i64(Config::LastAutorelay).await?;
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
@@ -203,7 +218,9 @@ async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
assert!(relay_added);
|
||||
|
||||
let config_after = t.get_config_i64(Config::LastAutorelay).await?;
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
@@ -236,7 +253,9 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
assert_eq!(relay_added, false);
|
||||
|
||||
// The config is still updated:
|
||||
let config_after = t.get_config_i64(Config::LastAutorelay).await?;
|
||||
let config_after = t
|
||||
.get_config_i64(Config::LastAutomaticRelayManagement)
|
||||
.await?;
|
||||
assert!(config_after >= now);
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
@@ -267,7 +286,7 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
|
||||
async fn enable_config(context: &Context) {
|
||||
context
|
||||
.set_config_bool(Config::Autorelay, true)
|
||||
.set_config_bool(Config::AutomaticRelayManagement, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -386,7 +386,7 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75; // 70-80 is the sweet spot of quality vs. bytes/pixel. if one wants to spend more bytes in quality, better increase resolution
|
||||
let jpeg_quality = 75;
|
||||
let ofmt = match fmt {
|
||||
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
ImageFormat::Jpeg => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user