Compare commits

..
57 changed files with 971 additions and 716 deletions
+5 -47
View File
@@ -29,7 +29,6 @@ jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -53,7 +52,6 @@ jobs:
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -68,7 +66,6 @@ jobs:
provider_database:
name: Check provider database
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -82,7 +79,6 @@ jobs:
docs:
name: Rust doc comments
runs-on: ubuntu-latest
timeout-minutes: 60
env:
RUSTDOCFLAGS: -Dwarnings
steps:
@@ -111,7 +107,6 @@ jobs:
- os: ubuntu-latest
rust: minimum
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- run:
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
@@ -160,7 +155,6 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -186,7 +180,6 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -209,7 +202,6 @@ jobs:
python_lint:
name: Python lint
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -227,38 +219,6 @@ jobs:
working-directory: deltachat-rpc-client
run: tox -e lint
# mypy does not work with PyPy since mypy 1.19
# as it introduced native `librt` dependency
# that uses CPython internals.
# We only run mypy with CPython because of this.
cffi_python_mypy:
name: CFFI Python mypy
needs: ["c_library", "python_lint"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v6
with:
name: ubuntu-latest-libdeltachat.a
path: target/debug
- name: Install tox
run: pip install tox
- name: Run mypy
env:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e mypy
cffi_python_tests:
name: CFFI Python tests
needs: ["c_library", "python_lint"]
@@ -278,14 +238,13 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.10
# Minimum Supported Python Version = 3.8
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: "3.10"
python: 3.8
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
@@ -312,7 +271,7 @@ jobs:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e doc,py
run: tox -e mypy,doc,py
rpc_python_tests:
name: JSON-RPC Python tests
@@ -334,12 +293,11 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.10
# Minimum Supported Python Version = 3.8
- os: ubuntu-latest
python: "3.10"
python: 3.8
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
+33 -142
View File
@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -46,30 +46,6 @@ jobs:
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_linux_wheel:
name: Linux wheel
strategy:
fail-fast: false
matrix:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
path: result/*.whl
if-no-files-found: error
build_windows:
name: Windows
strategy:
@@ -82,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -94,30 +70,6 @@ jobs:
path: result/bin/deltachat-rpc-server.exe
if-no-files-found: error
build_windows_wheel:
name: Windows wheel
strategy:
fail-fast: false
matrix:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
path: result/*.whl
if-no-files-found: error
build_macos:
name: macOS
strategy:
@@ -157,7 +109,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -169,33 +121,9 @@ jobs:
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_android_wheel:
name: Android wheel
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
path: result/*.whl
if-no-files-found: error
publish:
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
needs: ["build_linux", "build_windows", "build_macos"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
@@ -208,7 +136,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
@@ -216,84 +144,42 @@ jobs:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux aarch64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux-wheel
path: deltachat-rpc-server-aarch64-linux-wheel.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv7l wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux-wheel
path: deltachat-rpc-server-armv7l-linux-wheel.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux armv6l wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux-wheel
path: deltachat-rpc-server-armv6l-linux-wheel.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux i686 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux-wheel
path: deltachat-rpc-server-i686-linux-wheel.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Linux x86_64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux-wheel
path: deltachat-rpc-server-x86_64-linux-wheel.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win32 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32-wheel
path: deltachat-rpc-server-win32-wheel.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download Win64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64-wheel
path: deltachat-rpc-server-win64-wheel.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
with:
@@ -312,24 +198,12 @@ jobs:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android wheel for arm64-v8a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-arm64-v8a-android-wheel
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Download Android wheel for armeabi-v7a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android-wheel
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
- name: Create bin/ directory
run: |
mkdir -p bin
@@ -348,21 +222,38 @@ jobs:
- name: List binaries
run: ls -l bin/
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.12
- name: Install wheel
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels
- name: Build deltachat-rpc-server Python wheels and source package
run: |
mkdir -p dist
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
@@ -380,7 +271,7 @@ jobs:
--repo ${{ github.repository }} \
bin/* dist/*
- name: Publish deltachat-rpc-server to PyPI
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +105,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- run: nix build .#${{ matrix.installable }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
-74
View File
@@ -1,78 +1,5 @@
# Changelog
## [2.29.0] - 2025-12-01
### API-Changes
- deltachat-rpc-client: Add Message.exists().
### Features / Changes
- [**breaking**] Increase backup version from 3 to 4.
- Hide `To` header in encrypted messages.
- `deltachat_rpc_client.Rpc` accepts `rpc_server_path` for using a particular deltachat-rpc-server ([#7493](https://github.com/chatmail/core/pull/7493)).
- Don't send `Chat-Group-Avatar` header in unencrypted groups.
- Don't update `self-{avatar,status}` from received messages ([#7002](https://github.com/chatmail/core/pull/7002)).
### Fixes
- `CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)` ([#7490](https://github.com/chatmail/core/pull/7490)).
- Use the same webxdc ratelimit for all email servers.
- Handle the case when account does not exist in `get_existing_msg_ids()`.
- Don't send self-avatar in unencrypted messages ([#7136](https://github.com/chatmail/core/pull/7136)).
- Do not configure folders during transport configuration.
- Upload sync messages only with the primary transport.
- Do not use deprecated ConfiguredProvider in get_configured_provider.
### Build system
- Make scripts for remote testing usable.
- Increase minimum supported Python version to 3.10.
- Use SPDX license expression in Python package metadata.
### CI
- Set timeout-minutes for all jobs in ci.yaml workflow.
- Do not install Python manually to bulid RPC server wheels.
- Do not build fake RPC server source packages.
- Build Python wheels in separate jobs.
### Refactor
- [**breaking**] Remove some unneeded stock strings ([#7496](https://github.com/chatmail/core/pull/7496)).
- Strike events in rpc-client request handling, get result from queue.
- Use ConfiguredProvider config directly when loading legacy settings.
- Remove update_icons and disable_server_delete migrations.
- Use `SYMMETRIC_KEY_ALGORITHM` constant in `symm_encrypt_message()`.
- Make signing key non-optional for `pk_encrypt`.
### Tests
- `test_remove_member_bcc`: Test unencrypted group as it was initially.
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.8.1 to 31.8.4.
- cargo: Bump hyper from 1.7.0 to 1.8.1.
- cargo: Bump human-panic from 2.0.3 to 2.0.4.
- cargo: Bump hyper-util from 0.1.17 to 0.1.18.
- cargo: Bump rusqlite from 0.36.0 to 0.37.0.
- cargo: Bump tokio-util from 0.7.16 to 0.7.17.
- cargo: Bump toml from 0.9.7 to 0.9.8.
- cargo: Bump proptest from 1.8.0 to 1.9.0.
- cargo: Bump parking_lot from 0.12.4 to 0.12.5.
- cargo: Bump syn from 2.0.106 to 2.0.110.
- cargo: Bump quick-xml from 0.38.3 to 0.38.4.
- cargo: Bump rustls-pki-types from 1.12.0 to 1.13.0.
- cargo: Bump nu-ansi-term from 0.50.1 to 0.50.3.
- cargo: Bump sanitize-filename from 0.5.0 to 0.6.0.
- cargo: Bump quote from 1.0.41 to 1.0.42.
- cargo: Bump libc from 0.2.176 to 0.2.177.
- cargo: Bump bytes from 1.10.1 to 1.11.0.
- cargo: Bump image from 0.25.8 to 0.25.9.
- cargo: Bump rand from 0.9.0 to 0.9.2 ([#7501](https://github.com/chatmail/core/pull/7501)).
- cargo: Bump tokio from 1.45.1 to 1.48.0.
## [2.28.0] - 2025-11-23
### API-Changes
@@ -7309,4 +7236,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
Generated
+182 -176
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.29.0"
version = "2.28.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
@@ -194,14 +194,14 @@ nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.9"
regex = "1.10"
rusqlite = "0.37"
sanitize-filename = "0.6"
rusqlite = "0.36"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.17"
tokio-util = "0.7.16"
tracing-subscriber = "0.3"
yerpc = "0.6.4"
+1 -1
View File
@@ -108,7 +108,7 @@ fn criterion_benchmark(c: &mut Criterion) {
pk_encrypt(
plain.clone(),
vec![black_box(key_pair.public.clone())],
key_pair.secret.clone(),
Some(key_pair.secret.clone()),
true,
true,
)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.29.0"
version = "2.28.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+24
View File
@@ -7215,6 +7215,11 @@ void dc_event_unref(dc_event_t* event);
/// Used as device message text.
#define DC_STR_SELF_DELETED_MSG_BODY 91
/// "'Delete messages from server' turned off as now all folders are affected."
///
/// Used as device message text.
#define DC_STR_SERVER_TURNED_OFF 92
/// "Message deletion timer is set to %1$s minutes."
///
/// Used in status messages.
@@ -7411,6 +7416,19 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
/// If you now send a message to a group, contacts there will automatically
/// replace the old with your new address.\n\n It's highly advised to set up
/// your old email provider to forward all emails to your new email address.
/// Otherwise you might miss messages of contacts who did not get your new
/// address yet." + the link to the AEAP blog post
///
/// As soon as there is a post about AEAP, the UIs should add it:
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
///
/// Used in a device message that explains AEAP.
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
/// "You changed group name from \"%1$s\" to \"%2$s\"."
///
/// `%1$s` will be replaced by the old group name.
@@ -7659,6 +7677,12 @@ void dc_event_unref(dc_event_t* event);
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
/// @deprecated 2025-07
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message."
///
/// Used as the first info messages in newly created groups.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.29.0"
version = "2.28.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+13 -18
View File
@@ -121,14 +121,14 @@ impl CommandApi {
}
}
async fn get_context_opt(&self, id: u32) -> Option<deltachat::context::Context> {
self.accounts.read().await.get_account(id)
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
self.get_context_opt(id)
let sc = self
.accounts
.read()
.await
.ok_or_else(|| anyhow!("account with id {id} not found"))
.get_account(id)
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
Ok(sc)
}
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
@@ -1307,18 +1307,13 @@ impl CommandApi {
///
/// Returns IDs of existing messages.
async fn get_existing_msg_ids(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<Vec<u32>> {
if let Some(context) = self.get_context_opt(account_id).await {
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
Ok(existing_msg_ids
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect())
} else {
// Account does not exist, so messages do not exist either,
// but this is not an error.
Ok(Vec::new())
}
let context = self.get_context(account_id).await?;
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
Ok(existing_msg_ids
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect())
}
async fn get_message_list_items(
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.29.0"
"version": "2.28.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.29.0"
version = "2.28.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+6 -4
View File
@@ -1,18 +1,20 @@
[build-system]
requires = ["setuptools>=77"]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.29.0"
license = "MPL-2.0"
version = "2.28.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -22,7 +24,7 @@ classifiers = [
"Topic :: Communications :: Email"
]
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.8"
[tool.setuptools.package-data]
deltachat_rpc_client = [
@@ -60,10 +60,6 @@ class Message:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
def exists(self) -> bool:
"""Return True if the message exists."""
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
"""Continue the Autocrypt Setup Message key transfer.
@@ -4,8 +4,11 @@ from __future__ import annotations
import os
import random
import subprocess
import sys
from typing import AsyncGenerator, Optional
import execnet
import py
import pytest
@@ -197,3 +200,102 @@ def log():
print(" " + msg)
return Printer()
#
# support for testing against different deltachat-rpc-server/clients
# installed into a temporary virtualenv and connected via 'execnet' channels
#
@pytest.fixture(scope="session")
def get_core_python_env(tmp_path_factory):
envs = {}
def get_core_python(core_version):
venv = envs.get(core_version)
if venv:
return venv
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
python = sys.executable
subprocess.check_call([python, "-m", "venv", venv])
pip = venv.joinpath("bin", "pip")
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}"]
subprocess.check_call([pip, "install", "pytest"] + pkgs)
envs[core_version] = venv
return venv
return get_core_python
@pytest.fixture
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
that can use the `bob` online account, and the `bob_contact_alice`.
"""
def factory(core_version):
venv = get_core_python_env(core_version)
python = venv.joinpath("bin", "python")
gw = execnet.makegateway(f"popen//python={python}")
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(venv.joinpath("bin", "deltachat-rpc-server")), cm))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
sysinfo = channel.receive()
assert sysinfo == f"v{core_version}"
bob_vcard = channel.receive()
[alice_contact_bob] = alice.import_vcard(bob_vcard)
def eval(eval_str):
channel.send(eval_str)
return channel.receive()
return alice, alice_contact_bob, eval
return factory
def remote_bob_loop(channel):
import os
import pathlib
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
bin_path = str(pathlib.Path(rpc_server_path).parent)
os.environ["PATH"] = bin_path + ":" + os.environ["PATH"]
rpc = Rpc(accounts_dir=accounts_dir)
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
acfactory = ACFactory(dc)
bob = acfactory.get_online_account()
alice_vcard = channel.receive()
[alice_contact] = bob.import_vcard(alice_vcard)
ns = {"bob": bob, "bob_contact_alice": alice_contact}
channel.send(bob.self_contact.make_vcard())
while 1:
eval_str = channel.receive()
res = eval(eval_str, ns)
try:
channel.send(res)
except Exception:
# some unserializable result
channel.send(None)
@@ -0,0 +1,28 @@
import pytest
@pytest.mark.parametrize("version", ["2.20.0", "2.10.0"])
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
qr_code = alice.get_qr_code()
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.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
msg = alice.wait_for_incoming_msg()
assert msg.get_snapshot().text == "hello"
@@ -1,18 +1,14 @@
import subprocess
import sys
from platform import system # noqa
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
@pytest.mark.skipif("system() == 'Windows'")
def test_install_venv_and_use_other_core(tmp_path):
venv = tmp_path.joinpath("venv1")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = venv / "bin" / "python"
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.20.0"])
python = sys.executable
subprocess.check_call([python, "-m", "venv", venv])
subprocess.check_call([venv / "bin" / "pip", "install", "deltachat-rpc-server==2.20.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=venv.joinpath("bin", "deltachat-rpc-server"))
with rpc:
+11 -20
View File
@@ -661,6 +661,8 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
contact = alice.create_contact(account)
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
@@ -676,7 +678,15 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
alice_group.send_file(str(path))
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
assert snapshot.chat == bob_group
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory):
@@ -993,22 +1003,3 @@ def test_background_fetch(acfactory, dc):
snapshot = messages[-1].get_snapshot()
if snapshot.text == "Hello again!":
break
def test_message_exists(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
message1 = chat.send_text("Hello!")
message2 = chat.send_text("Hello again!")
assert message1.exists()
assert message2.exists()
ac1.delete_messages([message1])
assert not message1.exists()
assert message2.exists()
# There is no error when checking if
# the message exists for deleted account.
ac1.remove()
assert not message1.exists()
assert not message2.exists()
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.29.0"
version = "2.28.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.29.0"
"version": "2.28.0"
}
+1 -1
View File
@@ -38,7 +38,6 @@ skip = [
{ name = "rand", version = "0.8.5" },
{ name = "rustix", version = "0.38.44" },
{ name = "serdect", version = "0.2.0" },
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
@@ -63,6 +62,7 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "zerocopy", version = "0.7.32" },
]
+5 -4
View File
@@ -1,20 +1,20 @@
[build-system]
requires = ["setuptools>=77", "wheel", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.29.0"
license = "MPL-2.0"
version = "2.28.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.8"
authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
@@ -23,6 +23,7 @@ classifiers = [
dependencies = [
"cffi>=1.0.0",
"imap-tools",
"importlib_metadata;python_version<'3.8'",
"pluggy",
"requests",
]
+1 -1
View File
@@ -1 +1 @@
2025-12-01
2025-11-23
+2 -2
View File
@@ -26,10 +26,10 @@ and an own build machine.
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
- `remote_tests_python.sh` rsyncs to a build machine and runs
JSON-RPC Python tests remotely on the build machine.
`run-python-test.sh` remotely on the build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
Rust tests remotely on the build machine.
`run-rust-test.sh` remotely on the build machine.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
+28 -15
View File
@@ -1,32 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
#!/bin/bash
set -x
if ! test -v SSHTARGET; then
echo >&2 SSHTARGET is not set
exit 1
fi
BUILDDIR=ci_builds/chatmailcore
BUILD_ID=${1:?specify build ID}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
echo "--- Running Python tests remotely"
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`command -v sccache\`
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=release
export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN
scripts/make-rpc-testenv.sh
. venv/bin/activate
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv
#virtualenv -q -p python3.7 venv
#source venv/bin/activate
#pip install -q tox virtualenv
cd deltachat-rpc-client
pytest -n6 $@
set -x
which python
source \$HOME/venv/bin/activate
which python
bash scripts/run-python-test.sh
_HERE
+15 -11
View File
@@ -1,25 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
#!/bin/bash
if ! test -v SSHTARGET; then
echo >&2 SSHTARGET is not set
exit 1
fi
BUILDDIR=ci_builds/chatmailcore
BUILD_ID=${1:?specify build ID}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
set -e
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running Rust tests remotely"
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`command -v sccache\`
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
cargo nextest run
bash scripts/run-rust-test.sh
_HERE
+1 -1
View File
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py310,py311,py312,py313,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
+4 -6
View File
@@ -173,8 +173,11 @@ async fn test_selfavatar_outside_blobdir() {
.unwrap();
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
let avatar_path = Path::new(&avatar_blob);
assert!(
avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
"The avatar filename should be its hash, put instead it's {avatar_blob}"
);
let scaled_avatar_size = file_size(avatar_path).await;
info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
check_image_size(avatar_src, 1000, 1000);
@@ -184,11 +187,6 @@ async fn test_selfavatar_outside_blobdir() {
constants::BALANCED_AVATAR_SIZE,
);
assert!(
avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
"The avatar filename should be its hash, but instead it's {avatar_blob}"
);
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
let viewtype = &mut Viewtype::Image;
let strict_limits = true;
+4 -10
View File
@@ -3730,16 +3730,10 @@ pub(crate) async fn add_contact_to_chat_ex(
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
"Cannot add SELF to broadcast channel."
);
match chat.is_encrypted(context).await? {
true => ensure!(
contact.is_key_contact(),
"Only key-contacts can be added to encrypted chats"
),
false => ensure!(
!contact.is_key_contact(),
"Only address-contacts can be added to unencrypted chats"
),
}
ensure!(
chat.is_encrypted(context).await? == contact.is_key_contact(),
"Only key-contacts can be added to encrypted chats"
);
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
+13 -1
View File
@@ -815,6 +815,15 @@ async fn test_self_talk() -> Result<()> {
assert!(msg.get_showpadlock());
let sent_msg = t.pop_sent_msg().await;
let payload = sent_msg.payload();
// Make sure the `To` field contains the address and not
// "undisclosed recipients".
// Otherwise Delta Chat core <1.153.0 assigns the message
// to the trash chat.
assert_eq!(
payload.match_indices("To: <alice@example.org>\r\n").count(),
1
);
let t2 = TestContext::new_alice().await;
t2.recv_msg(&sent_msg).await;
@@ -3312,7 +3321,10 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
let leave_msg = bob0.pop_sent_msg().await;
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
assert_eq!(parsed.parts[0].msg, "I left the group.");
assert_eq!(
parsed.parts[0].msg,
stock_str::msg_group_left_remote(bob0).await
);
let rcvd = bob1.recv_msg(&leave_msg).await;
+8 -7
View File
@@ -18,7 +18,7 @@ use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::Provider;
use crate::provider::{Provider, get_provider_by_id};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::ConfiguredLoginParam;
@@ -646,14 +646,15 @@ impl Context {
Ok(val)
}
/// Gets the configured provider.
/// Gets the configured provider, as saved in the `configured_provider` value.
///
/// The provider is determined by the current primary transport.
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
let provider = ConfiguredLoginParam::load(self)
.await?
.and_then(|(_transport_id, param)| param.provider);
Ok(provider)
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
}
/// Gets configured "delete_device_after" value.
+4 -3
View File
@@ -278,6 +278,7 @@ async fn test_sync() -> Result<()> {
Ok(())
}
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -287,7 +288,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Sent via usual message";
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
@@ -296,7 +297,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status1.to_string())
Some(status.to_string())
);
sync(alice1, alice0).await;
assert_eq!(
@@ -327,7 +328,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.filter(|path| path.ends_with(".png"))
.is_some()
);
sync(alice1, alice0).await;
+26 -5
View File
@@ -27,7 +27,7 @@ use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::warn;
use crate::log::{LogExt, warn};
use crate::login_param::EnteredCertificateChecks;
pub use crate::login_param::EnteredLoginParam;
use crate::message::Message;
@@ -44,6 +44,7 @@ use crate::transport::{
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
@@ -266,7 +267,7 @@ impl Context {
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, provider).await?;
on_configure_completed(self, provider, old_addr).await?;
Ok(())
}
}
@@ -274,6 +275,7 @@ impl Context {
async fn on_configure_completed(
context: &Context,
provider: Option<&'static Provider>,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
@@ -303,6 +305,20 @@ async fn on_configure_completed(
}
}
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await?
&& let Some(old_addr) = old_addr
&& !addr_cmp(&new_addr, &old_addr)
{
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
}
Ok(())
}
@@ -538,11 +554,12 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
let (_s, r) = async_channel::bounded(1);
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
let configuring = true;
if let Err(err) = imap.connect(ctx, configuring).await {
bail!(
let mut imap_session = match imap.connect(ctx, configuring).await {
Ok(session) => session,
Err(err) => bail!(
"{}",
nicer_configuration_error(ctx, format!("{err:#}")).await
);
),
};
progress!(ctx, 850);
@@ -557,6 +574,10 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
let create_mvbox = false;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
drop(imap);
progress!(ctx, 910);
+51 -19
View File
@@ -383,16 +383,20 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
},
None => None,
};
if let Some(path) = path
&& let Err(e) = set_profile_image(context, id, &AvatarAction::Change(path)).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
if let Some(path) = path {
// Currently this value doesn't matter as we don't import the contact of self.
let was_encrypted = false;
if let Err(e) =
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
}
}
if let Some(biography) = &contact.biography
&& let Err(e) = set_status(context, id, biography.to_owned()).await
&& let Err(e) = set_status(context, id, biography.to_owned(), false, false).await
{
warn!(
context,
@@ -1505,6 +1509,18 @@ impl Contact {
&self.addr
}
/// Get authorized name or address.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
pub(crate) fn get_authname_or_addr(&self) -> String {
if !self.authname.is_empty() {
(&self.authname).into()
} else {
(&self.addr).into()
}
}
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
@@ -1820,19 +1836,25 @@ WHERE type=? AND id IN (
/// The given profile image is expected to be already in the blob directory
/// as profile images can be set only by receiving messages, this should be always the case, however.
///
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar.
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
/// this typically happens if we see message with our own profile image.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
let mut contact = Contact::get_by_id(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
context
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.await?;
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar.");
}
} else {
contact.param.set(Param::ProfileImage, profile_image);
}
@@ -1840,9 +1862,13 @@ pub(crate) async fn set_profile_image(
}
AvatarAction::Delete => {
if contact_id == ContactId::SELF {
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.await?;
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar deletion.");
}
} else {
contact.param.remove(Param::ProfileImage);
}
@@ -1859,16 +1885,22 @@ pub(crate) async fn set_profile_image(
/// Sets contact status.
///
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus.
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus. This
/// is only done if message is sent from Delta Chat and it is encrypted, to synchronize signature
/// between Delta Chat devices.
pub(crate) async fn set_status(
context: &Context,
contact_id: ContactId,
status: String,
encrypted: bool,
has_chat_version: bool,
) -> Result<()> {
if contact_id == ContactId::SELF {
context
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.await?;
if encrypted && has_chat_version {
context
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.await?;
}
} else {
let mut contact = Contact::get_by_id(context, contact_id).await?;
+36 -8
View File
@@ -4,7 +4,7 @@ use super::*;
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
#[test]
fn test_contact_id_values() {
@@ -850,7 +850,8 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
/// Tests that self-status is not synchronized from outgoing messages.
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
/// synchronized when the message is not encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_synchronize_status() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -869,12 +870,21 @@ async fn test_synchronize_status() -> Result<()> {
.await?;
let chat = alice1.create_email_chat(bob).await;
// Alice sends an unencrypted message to Bob from the first device.
// Alice sends a message to Bob from the first device.
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
let message = sent_msg.load_from_db().await;
assert!(!message.get_showpadlock());
// Alice's second devices receives a copy of outgoing message.
alice2.recv_msg(&sent_msg).await;
// Bob receives message.
bob.recv_msg(&sent_msg).await;
// Message was not encrypted, so status is not copied.
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
// Alice sends encrypted message.
@@ -882,9 +892,17 @@ async fn test_synchronize_status() -> Result<()> {
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second devices receives a copy of second outgoing message.
alice2.recv_msg(&sent_msg).await;
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
assert_eq!(
alice2.get_config(Config::Selfstatus).await?,
Some("New status".to_string())
);
Ok(())
}
@@ -897,9 +915,9 @@ async fn test_selfavatar_changed_event() -> Result<()> {
// Alice has two devices.
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
for a in [alice1, alice2] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
// Bob has one device.
let bob = &tcm.bob().await;
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
@@ -915,7 +933,17 @@ async fn test_selfavatar_changed_event() -> Result<()> {
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
.await;
sync(alice1, alice2).await;
// Alice sends a message.
let alice1_chat_id = alice1.create_chat(bob).await.id;
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second device receives a copy of the outgoing message.
alice2.recv_msg(&sent_msg).await;
// Alice's second device applies the selfavatar.
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
+7 -1
View File
@@ -453,7 +453,7 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
quota: RwLock::new(None),
new_msgs_notify,
server_id: RwLock::new(None),
@@ -485,6 +485,12 @@ impl Context {
return;
}
if self.is_chatmail().await.unwrap_or_default() {
let mut lock = self.ratelimit.write().await;
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
+1 -1
View File
@@ -57,7 +57,7 @@ impl EncryptHelper {
let ctext = pgp::pk_encrypt(
raw_message,
keyring,
sign_key,
Some(sign_key),
compress,
anonymous_recipients,
)
+1 -1
View File
@@ -79,7 +79,7 @@ pub(crate) struct Imap {
pub(crate) idle_interrupt_receiver: Receiver<()>,
/// Email address.
pub(crate) addr: String,
addr: String,
/// Login parameters.
lp: Vec<ConfiguredServerLoginParam>,
+27 -14
View File
@@ -9,7 +9,7 @@ use data_encoding::BASE32_NOPAD;
use deltachat_contact_tools::sanitize_bidi_characters;
use iroh_gossip::proto::TopicId;
use mail_builder::headers::HeaderType;
use mail_builder::headers::address::Address;
use mail_builder::headers::address::{Address, EmailAddress};
use mail_builder::mime::MimePart;
use tokio::fs;
@@ -464,11 +464,7 @@ impl MimeFactory {
.unwrap_or_default(),
false => "".to_string(),
};
// We don't display avatars for address-contacts, so sending avatars w/o encryption is not
// useful and causes e.g. Outlook to reject a message with a big header, see
// https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
let attach_selfavatar =
Self::should_attach_selfavatar(context, &msg).await && encryption_pubkeys.is_some();
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
ensure_and_debug_assert!(
member_timestamps.is_empty()
@@ -997,7 +993,24 @@ impl MimeFactory {
} else if header_name == "to" {
protected_headers.push(header.clone());
if is_encrypted {
unprotected_headers.push(("To", hidden_recipients().into()));
let mut to_without_names = to
.clone()
.into_iter()
.filter_map(|header| match header {
Address::Address(mb) => Some(Address::Address(EmailAddress {
name: None,
email: mb.email,
})),
_ => None,
})
.collect::<Vec<_>>();
if to_without_names.is_empty() {
to_without_names.push(hidden_recipients());
}
unprotected_headers.push((
original_header_name,
Address::new_list(to_without_names).into(),
));
} else {
unprotected_headers.push(header.clone());
}
@@ -1525,9 +1538,10 @@ impl MimeFactory {
.await?
.unwrap_or_default()
{
placeholdertext = Some("I left the group.".to_string());
placeholdertext = Some(stock_str::msg_group_left_remote(context).await);
} else {
placeholdertext = Some(format!("I removed member {email_to_remove}."));
placeholdertext =
Some(stock_str::msg_del_member_remote(context, email_to_remove).await);
};
if !email_to_remove.is_empty() {
@@ -1550,7 +1564,8 @@ impl MimeFactory {
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
let fingerprint_to_add = msg.param.get(Param::Arg4).unwrap_or_default();
placeholdertext = Some(format!("I added member {email_to_add}."));
placeholdertext =
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
if !email_to_add.is_empty() {
headers.push((
@@ -1586,7 +1601,7 @@ impl MimeFactory {
"Chat-Content",
mail_builder::headers::text::Text::new("group-avatar-changed").into(),
));
if grpimage.is_none() && is_encrypted {
if grpimage.is_none() {
headers.push((
"Chat-Group-Avatar",
mail_builder::headers::raw::Raw::new("0").into(),
@@ -1713,9 +1728,7 @@ impl MimeFactory {
_ => {}
}
if let Some(grpimage) = grpimage
&& is_encrypted
{
if let Some(grpimage) = grpimage {
info!(context, "setting group image '{}'", grpimage);
let avatar = build_avatar_file(context, grpimage)
.await
+82 -15
View File
@@ -6,8 +6,7 @@ use std::time::Duration;
use super::*;
use crate::chat::{
self, ChatId, add_contact_to_chat, create_group, create_group_unencrypted,
remove_contact_from_chat, send_text_msg,
self, ChatId, add_contact_to_chat, create_group, remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::constants;
@@ -352,7 +351,7 @@ async fn test_subject_in_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = tcm.alice().await;
let bob = tcm.bob().await;
let group_id = create_group(&t, "groupname").await.unwrap();
let group_id = chat::create_group(&t, "groupname").await.unwrap();
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
@@ -593,6 +592,26 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("text/plain").count(), 1);
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(inner.match_indices("Subject:").count(), 0);
assert_eq!(body.match_indices("this is the text!").count(), 1);
// if another message is sent, that one must not contain the avatar
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
let outer = payload.next().unwrap();
let inner = payload.next().unwrap();
let body = payload.next().unwrap();
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
assert_eq!(outer.match_indices("Subject:").count(), 1);
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("text/plain").count(), 1);
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
@@ -651,7 +670,7 @@ async fn test_selfavatar_unencrypted_signed() {
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(part.match_indices("From:").count(), 0);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
@@ -665,6 +684,58 @@ async fn test_selfavatar_unencrypted_signed() {
.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
assert!(
alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some()
);
// if another message is sent, that one must not contain the avatar
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/signed").count(), 1);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(body.match_indices("From:").count(), 0);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
bob.recv_msg(&sent_msg).await;
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert!(
alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some()
);
}
/// Test that removed member address does not go into the `To:` field.
@@ -672,20 +743,17 @@ async fn test_selfavatar_unencrypted_signed() {
async fn test_remove_member_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice creates a group with Bob and Charlie and then removes Charlie.
// Alice creates a group with Bob and Claire and then removes Bob.
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
let charlie_addr = charlie_contact.get_addr();
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
let alice_chat_id = create_group_unencrypted(alice, "foo").await?;
let alice_chat_id = create_group(alice, "foo").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
@@ -702,9 +770,8 @@ async fn test_remove_member_bcc() -> Result<()> {
for to_addr in to.iter() {
match to_addr {
mailparse::MailAddr::Single(info) => {
// Addresses should be of existing members and not Charlie.
// Addresses should be of existing members (Alice and Bob) and not Charlie.
assert_ne!(info.addr, charlie_addr);
assert!(info.addr == alice_addr || info.addr == bob_addr);
}
mailparse::MailAddr::Group(_) => {
panic!("Group addresses are not expected here");
+43 -16
View File
@@ -165,7 +165,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
pub async fn pk_encrypt(
plain: Vec<u8>,
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: SignedSecretKey,
private_key_for_signing: Option<SignedSecretKey>,
compress: bool,
anonymous_recipients: bool,
) -> Result<String> {
@@ -187,9 +187,11 @@ pub async fn pk_encrypt(
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
if let Some(ref skey) = private_key_for_signing {
msg.sign(&**skey, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
@@ -404,7 +406,7 @@ pub async fn symm_encrypt_message(
};
let mut msg = msg.seipd_v2(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
SymmetricKeyAlgorithm::AES128,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
@@ -532,6 +534,7 @@ mod tests {
static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
/// A ciphertext encrypted to Alice & Bob, signed by Alice.
async fn ctext_signed() -> &'static String {
@@ -544,7 +547,28 @@ mod tests {
pk_encrypt(
CLEARTEXT.to_vec(),
keyring,
KEYS.alice_secret.clone(),
Some(KEYS.alice_secret.clone()),
compress,
anonymous_recipients,
)
.await
.unwrap()
})
.await
}
/// A ciphertext encrypted to Alice & Bob, not signed.
async fn ctext_unsigned() -> &'static String {
let anonymous_recipients = true;
CTEXT_UNSIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(
CLEARTEXT.to_vec(),
keyring,
None,
compress,
anonymous_recipients,
)
@@ -564,6 +588,16 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_unsigned() {
assert!(!ctext_unsigned().await.is_empty());
assert!(
ctext_unsigned()
.await
.starts_with("-----BEGIN PGP MESSAGE-----")
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_singed() {
// Check decrypting as Alice
@@ -618,9 +652,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let ctext_unsigned = include_bytes!("../test-data/message/ctext_unsigned.asc");
let (_msg, valid_signatures, content) =
pk_decrypt_and_validate(ctext_unsigned, &decrypt_keyring, &[]).unwrap();
pk_decrypt_and_validate(ctext_unsigned().await.as_bytes(), &decrypt_keyring, &[])
.unwrap();
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -710,14 +744,7 @@ mod tests {
let pk_for_encryption = load_self_public_key(alice).await?;
// Encrypt a message, but only to self, not to Bob:
let ctext = pk_encrypt(
plain,
vec![pk_for_encryption],
KEYS.alice_secret.clone(),
true,
true,
)
.await?;
let ctext = pk_encrypt(plain, vec![pk_for_encryption], None, true, true).await?;
// Trying to decrypt it should fail with an OK error message:
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
+1 -1
View File
@@ -42,7 +42,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
/// Version written to Backups and Backup-QR-Codes.
/// Imports will fail when they have a larger version.
pub(crate) const DCBACKUP_VERSION: i32 = 4;
pub(crate) const DCBACKUP_VERSION: i32 = 3;
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]
+36
View File
@@ -991,6 +991,42 @@ Content-Disposition: reaction\n\
Ok(())
}
/// Regression test for reaction resetting self-status.
///
/// Reactions do not contain the status,
/// but should not result in self-status being reset on other devices.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_status_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice1 = tcm.alice().await;
let alice2 = tcm.alice().await;
alice1
.set_config(Config::Selfstatus, Some("New status"))
.await?;
let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
assert_eq!(
alice2.get_config(Config::Selfstatus).await?.as_deref(),
Some("New status")
);
// Alice reacts to own message from second device,
// first device receives rection.
{
send_reaction(&alice2, alice2_msg.id, "👍").await?;
let msg = alice2.pop_sent_msg().await;
alice1.recv_msg_hidden(&msg).await;
}
// Check that the status is still the same.
assert_eq!(
alice1.get_config(Config::Selfstatus).await?.as_deref(),
Some("New status")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
+13 -4
View File
@@ -885,11 +885,13 @@ pub(crate) async fn receive_imf_inner(
}
if let Some(avatar_action) = &mime_parser.user_avatar
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
&& let Err(err) =
contact::set_profile_image(context, from_id, avatar_action, mime_parser.was_encrypted())
.await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
@@ -897,11 +899,18 @@ pub(crate) async fn receive_imf_inner(
// Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
if let Some(footer) = &mime_parser.footer
&& !mime_parser.is_mailinglist_message()
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
&& let Err(err) = contact::set_status(
context,
from_id,
footer.to_string(),
mime_parser.was_encrypted(),
mime_parser.has_chat_version(),
)
.await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
+6 -13
View File
@@ -578,19 +578,12 @@ async fn fetch_idle(
mvbox.as_deref().unwrap_or(&watch_folder)
}
};
if ctx
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default()
== connection.addr
{
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
}
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
session
.store_seen_flags_on_imap(ctx)
+17 -2
View File
@@ -9,6 +9,7 @@ use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
use crate::chat::add_device_msg;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::context::Context;
@@ -17,11 +18,12 @@ use crate::ephemeral::start_ephemeral_timers;
use crate::imex::BLOBS_BACKUP_NAME;
use crate::location::delete_orphaned_poi_locations;
use crate::log::{LogExt, warn};
use crate::message::MsgId;
use crate::message::{Message, MsgId};
use crate::net::dns::prune_dns_cache;
use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::stock_str;
use crate::tools::{SystemTime, Time, delete_file, time, time_elapsed};
/// Extension to [`rusqlite::ToSql`] trait
@@ -214,13 +216,26 @@ impl Sql {
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
let recode_avatar = migrations::run(context, self)
// `update_icons` is not used anymore, since it's not necessary anymore to "update" icons:
let (_update_icons, disable_server_delete, recode_avatar) = migrations::run(context, self)
.await
.context("failed to run migrations")?;
// (2) updates that require high-level objects
// the structure is complete now and all objects are usable
if disable_server_delete {
// We now always watch all folders and delete messages there if delete_server is enabled.
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new_text(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config_internal(Config::DeleteServerAfter, Some("0"))
.await?;
}
}
if recode_avatar && let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
match blob.recode_to_avatar_size(context).await {
+17 -11
View File
@@ -31,7 +31,7 @@ tokio::task_local! {
static STOP_MIGRATIONS_AT: i32;
}
pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
let mut exists_before_update = false;
let mut dbversion_before_update = DBVERSION;
@@ -68,6 +68,8 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
}
let dbversion = dbversion_before_update;
let mut update_icons = !exists_before_update;
let mut disable_server_delete = false;
let mut recode_avatar = false;
if dbversion < 1 {
@@ -297,6 +299,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
61,
)
.await?;
update_icons = true;
}
if dbversion < 62 {
sql.execute_migration(
@@ -324,6 +327,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 66 {
update_icons = true;
sql.set_db_version(66).await?;
}
if dbversion < 67 {
@@ -441,6 +445,17 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
}
}
}
if exists_before_update {
disable_server_delete = true;
// Don't disable server delete if it was on by default (Nauta):
if let Some(provider) = context.get_configured_provider().await?
&& let Some(defaults) = &provider.config_defaults
&& defaults.iter().any(|d| d.key == Config::DeleteServerAfter)
{
disable_server_delete = false;
}
}
sql.set_db_version(73).await?;
}
if dbversion < 74 {
@@ -1430,15 +1445,6 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
.await?;
}
inc_and_check(&mut migration_version, 141)?;
if dbversion < migration_version {
sql.execute_migration(
"CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -1453,7 +1459,7 @@ CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
}
info!(context, "Database version: v{new_version}.");
Ok(recode_avatar)
Ok((update_icons, disable_server_delete, recode_avatar))
}
fn migrate_key_contacts(
+3 -2
View File
@@ -1,5 +1,4 @@
use super::*;
use crate::message::Message;
use crate::{EventType, test_utils::TestContext};
#[test]
@@ -180,7 +179,9 @@ async fn test_migration_flags() -> Result<()> {
// as migrations::run() was already executed on context creation,
// another call should not result in any action needed.
// this test catches some bugs where dbversion was forgotten to be persisted.
let recode_avatar = migrations::run(&t, &t.sql).await?;
let (update_icons, disable_server_delete, recode_avatar) = migrations::run(&t, &t.sql).await?;
assert!(!update_icons);
assert!(!disable_server_delete);
assert!(!recode_avatar);
info!(&t, "test_migration_flags: XXX END MARKER");
+84 -1
View File
@@ -13,7 +13,7 @@ use crate::accounts::Accounts;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::param::Param;
@@ -155,6 +155,13 @@ pub enum StockMessage {
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
SelfDeletedMsgBody = 91,
#[strum(props(
fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
))]
DeleteServerTurnedOff = 92,
#[strum(props(fallback = "Forwarded"))]
Forwarded = 97,
@@ -234,6 +241,11 @@ pub enum StockMessage {
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
#[strum(props(
fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
))]
AeapExplanationAndLink = 123,
#[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
MsgYouChangedGrpName = 124,
@@ -344,9 +356,22 @@ pub enum StockMessage {
#[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
BackupTransferMsgBody = 163,
#[strum(props(fallback = "I added member %1$s."))]
MsgIAddMember = 164,
#[strum(props(fallback = "I removed member %1$s."))]
MsgIDelMember = 165,
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
ChatProtectionEnabled = 170,
// deprecated 2025-07
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
NewGroupSendFirstMessage = 172,
@@ -601,6 +626,25 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
}
}
/// Stock string: `I added member %1$s.`.
/// This one is for sending in group chats.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
/// contacts to combine with the authorized display name.
pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
let addr = added_member_addr;
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_authname_or_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIAddMember)
.await
.replace1(whom)
}
/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
@@ -627,6 +671,24 @@ pub(crate) async fn msg_add_member_local(
}
}
/// Stock string: `I removed member %1$s.`.
///
/// The `removed_member_addr` parameter should be an email address and is looked up in
/// the contacts to combine with the display name.
pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
let addr = removed_member_addr;
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await
.map(|contact| contact.get_authname_or_addr())
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
translated(context, StockMessage::MsgIDelMember)
.await
.replace1(whom)
}
/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
///
/// The `removed_member_addr` parameter should be an email address and is looked up in
@@ -653,6 +715,11 @@ pub(crate) async fn msg_del_member_local(
}
}
/// Stock string: `I left the group.`.
pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
/// Stock string: `You left the group.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
@@ -1034,6 +1101,11 @@ pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
translated(context, StockMessage::SelfDeletedMsgBody).await
}
/// Stock string: `⚠️ The "Delete messages from server" feature now also...`.
pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
translated(context, StockMessage::DeleteServerTurnedOff).await
}
/// Stock string: `Message deletion timer is set to %1$s minutes.`.
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
@@ -1221,6 +1293,17 @@ pub(crate) async fn stats_msg_body(context: &Context) -> String {
translated(context, StockMessage::StatsMsgBody).await
}
pub(crate) async fn aeap_explanation_and_link(
context: &Context,
old_addr: &str,
new_addr: &str,
) -> String {
translated(context, StockMessage::AeapExplanationAndLink)
.await
.replace1(old_addr)
.replace2(new_addr)
}
/// Stock string: `Others will only see this group after you sent a first message.`.
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
translated(context, StockMessage::NewGroupSendFirstMessage).await
+8
View File
@@ -75,6 +75,10 @@ async fn test_stock_system_msg_add_member_by_me() {
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
.await
.expect("failed to create contact");
assert_eq!(
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
"You added member Alice."
@@ -87,6 +91,10 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() {
let alice_contact_id = Contact::create(&t, "Alice", "alice@example.org")
.await
.expect("failed to create contact");
assert_eq!(
msg_add_member_remote(&t, "alice@example.org").await,
"I added member alice@example.org."
);
assert_eq!(
msg_add_member_local(&t, alice_contact_id, ContactId::SELF).await,
"You added member Alice."
-13
View File
@@ -33,7 +33,6 @@ use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, DcSecretKey, self_fingerprint};
use crate::log::warn;
use crate::login_param::EnteredLoginParam;
use crate::message::{Message, MessageState, MsgId, update_msg_state};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::pgp::KeyPair;
@@ -201,18 +200,6 @@ impl TestContextManager {
"{} changes her self address and reconfigures",
test_context.name()
));
// Insert a transport for the new address.
test_context.sql
.execute(
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
new_addr,
serde_json::to_string(&EnteredLoginParam::default()).unwrap(),
format!(r#"{{"addr":"{new_addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
).await.unwrap();
test_context.set_primary_self_addr(new_addr).await.unwrap();
// ensure_secret_key_exists() is called during configure
crate::e2ee::ensure_secret_key_exists(test_context)
+1 -4
View File
@@ -323,10 +323,7 @@ impl ConfiguredLoginParam {
.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
let provider = context
.get_config(Config::ConfiguredProvider)
.await?
.and_then(|cfg| get_provider_by_id(&cfg));
let provider = context.get_configured_provider().await?;
let imap;
let smtp;
+1 -1
View File
@@ -1289,7 +1289,7 @@ async fn test_get_webxdc_info() -> Result<()> {
let info = instance.get_webxdc_info(&t).await?;
assert_eq!(info.name, "minimal.xdc");
assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string());
assert_eq!(info.send_update_interval, 1000);
assert_eq!(info.send_update_interval, 10000);
assert_eq!(info.send_update_max_size, RECOMMENDED_FILE_SIZE as usize);
let mut instance = create_webxdc_instance(
-13
View File
@@ -1,13 +0,0 @@
-----BEGIN PGP MESSAGE-----
wU4DAAAAAAAAAAASAQdA4GtzIk21tMpcktf+k47Gn8iSt8JMJgu/hEf7wJLUGgsg
/dQ8eBSe4/PoEoNQ2mHnHWSdPH4zvS142R86iiyGe3jBwEwDAAAAAAAAAAABB/9l
dteYjCvGpJxjgm8dGcb3lY9jVj76HLu72KWLZy2p/Ch/mxGW2GtgWNipFA2plCLQ
Dzutak20lbYXkBL+zEnioHc5aAixruQQa2yZSRqePxPmkC/eTiedNeVJCOcrJBbj
k8kQ+CPXLxx6mWS28s0tQkbpH9oJ2Esy0J6FNl0DMTWG50FWuo+Mh/gQ7Su3S1Il
MYa7n58LhMjNGwD0ckp+Ig1XPNnnyauA+0kU/yDy7aA9NH4CtdqeobitEnn2aK4c
sge0008GzZ/p1UWAyIVCqXuB1cNQY0UMVLohV3v343Xf9vS6ZQ8Bi8L+DiS+WpSt
2fYGJ7Iqq6joatv6wPgS0j8BTbOcCygy/V1NvwzZpXnOawiGYJaCFU33vr2p1SrK
YB5YCwFI4499aXUGhokyAW0xy6+N7o2Ps1qS4L3cqu0=
=Ytf8
-----END PGP MESSAGE-----