Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
6a128cf04a feat: do not copy Chat-Version header into unprotected headers
spec.md already documents Header Confidentiality Policy
that hides Chat-Version header in encrypted messages,
but so far we have copied the header into unprotected headers for compatibility.

Since 2.45.0 prefetch does not look at the unprotected Chat-Version
do decide if the message should be downloaded
(change b94792706a)
and since 2.50 show_emails setting which controlled
whether only "chat messages" are processed
is removed (change 63596a4940).
2026-08-16 10:45:33 +00:00
185 changed files with 5518 additions and 5739 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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"]

View File

@@ -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

View File

@@ -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();

View File

@@ -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"]

View File

@@ -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
/**

View File

@@ -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],

View File

@@ -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))

View File

@@ -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"]

View File

@@ -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");
}

View File

@@ -1 +0,0 @@
generated

View File

@@ -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 *)

View File

@@ -1 +0,0 @@

View File

@@ -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"

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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,
}

View File

@@ -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();

View File

@@ -105,7 +105,6 @@ pub struct MessageObject {
is_pinned: bool,
/// `None` when there are no reactions.
reactions: Option<JsonrpcReactions>,
vcard_contact: Option<VcardContact>,

View File

@@ -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 {

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.61.0-dev"
"version": "2.60.0-dev"
}

View File

@@ -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"

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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 = [

View File

@@ -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.

View File

@@ -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"

View File

@@ -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()

View File

@@ -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))

View File

@@ -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))

View File

@@ -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."""

View File

@@ -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]:

View File

@@ -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")

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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"]))

View File

@@ -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()

View File

@@ -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"]

View File

@@ -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

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.61.0-dev"
"version": "2.60.0-dev"
}

View File

@@ -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"] = "*";
}

View File

@@ -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:

View File

@@ -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
View File

@@ -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
];
};
}
);
}

View File

@@ -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}";
}

View File

@@ -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'';
}

View File

@@ -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}";
}

View File

@@ -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
];
}

View File

@@ -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"'
'';
}

View File

@@ -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'';
}

View File

@@ -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
];
}

View File

@@ -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'';
}

View File

@@ -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}";
}

View File

@@ -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}";
}

View File

@@ -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::

View File

@@ -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"

View File

@@ -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.

View File

@@ -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.

View File

@@ -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]:

View File

@@ -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:

View File

@@ -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")

View File

@@ -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):

View File

@@ -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

View File

@@ -1 +1 @@
2026-09-11
2026-08-14

View File

@@ -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

View File

@@ -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",

View File

@@ -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
View File

@@ -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.

View File

@@ -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")?;

View File

@@ -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")
);
}
}

View File

@@ -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(())
}
}

View File

@@ -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, &param, 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;

View File

@@ -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();
}

View File

@@ -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