Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
2ec7c80c24 feat: display TLS certificate checks configuration in connectivity view 2025-10-13 19:51:44 +00:00
170 changed files with 4754 additions and 8644 deletions

View File

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

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.91.0
RUST_VERSION: 1.90.0
# Minimum Supported Rust Version
MSRV: 1.85.0
@@ -168,7 +168,7 @@ jobs:
run: cargo build -p deltachat_ffi
- name: Upload C library
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -193,7 +193,7 @@ jobs:
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -252,7 +252,7 @@ jobs:
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
@@ -313,7 +313,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -34,13 +34,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
@@ -58,13 +58,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
@@ -91,7 +91,7 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
@@ -109,13 +109,13 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
@@ -136,70 +136,70 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -294,67 +294,67 @@ jobs:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -384,7 +384,7 @@ jobs:
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
@@ -401,7 +401,7 @@ jobs:
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -19,7 +19,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -21,7 +21,7 @@ jobs:
show-progress: false
persist-credentials: false
- name: Use Node.js 18.x
uses: actions/setup-node@v6
uses: actions/setup-node@v5
with:
node-version: 18.x
- name: Add Rust cache

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # 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@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -95,15 +95,14 @@ jobs:
matrix:
installable:
- deltachat-rpc-server
- deltachat-rpc-server-x86_64-darwin
# Fails to build
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
# Fails to bulid
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -23,7 +23,7 @@ jobs:
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/

View File

@@ -18,11 +18,11 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: repl.exe
path: "result/bin/deltachat-repl.exe"

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@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # 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@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
@@ -78,7 +78,7 @@ jobs:
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v5
with:
node-version: '18'
- name: npm install

View File

@@ -19,13 +19,13 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41
uses: astral-sh/setup-uv@v6
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: zizmor

1
.gitignore vendored
View File

@@ -36,7 +36,6 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode
.zed
python/accounts.txt
python/all-testaccounts.txt
tmp/

View File

@@ -1,180 +1,5 @@
# Changelog
## [2.25.0] - 2025-11-05
### Features / Changes
- Put self-name into group invite codes ([#7398](https://github.com/chatmail/core/pull/7398)).
- Slightly nicer and shorter QR and invite codes ([#7390](https://github.com/chatmail/core/pull/7390))
### Fixes
- Add device message instead of partial message when receive_imf fails. This fixes a rare bug where the IMAP loop got stuck.
- Add info message if user tries to create a QR code for deprecated channel ([#7399](https://github.com/chatmail/core/pull/7399)).
### Miscellaneous Tasks
- deps: Bump actions/upload-artifact from 4 to 5.
- deps: Bump actions/download-artifact from 5 to 6.
- deps: Bump astral-sh/setup-uv from 7.1.0 to 7.1.2.
### Refactor
- sql: Do not expose rusqlite Error type in query_map methods.
## [2.24.0] - 2025-11-03
***Note that in v2.24.0, the IMAP loop can get stuck in rare circumstances;
use v2.23.0 or v2.25.0 instead.***
### Documentation
- Comment why spaced en dash is used to separate message Subject from text.
### Features / Changes
- [**breaking**] QR codes and symmetric encryption for broadcast channels ([#7268](https://github.com/chatmail/core/pull/7268)).
- A new QR type AskJoinBroadcast; cloning a broadcast
channel is no longer possible; manually adding a member to a broadcast
channel is no longer possible (the only way to join a channel is scanning a QR code or clicking a link)
### Refactor
- Split "transport" module out of "login_param".
## [2.23.0] - 2025-11-01
### API-Changes
- Make `dc_chat_is_protected` always return 0.
- [**breaking**] Remove public APIs to check if the chat is protected.
- [**breaking**] Remove APIs to create protected chats.
- [**breaking**] Remove Chat.is_protected().
- deltachat-rpc-client: Add Account.add_transport_from_qr() API.
- JSON-RPC: add `get_push_state` to check push notification state ([#7356](https://github.com/chatmail/core/pull/7356)).
- JSON-RPC: remove unused TypeScript constants ([#7355](https://github.com/chatmail/core/pull/7355)).
- Remove `Config::SentboxWatch` ([#7178](https://github.com/chatmail/core/pull/7178)).
- Remove `Config::ConfiguredSentboxFolder` and everything related.
### Build system
- Ignore configuration for the zed editor ([#7322](https://github.com/chatmail/core/pull/7322)).
- nix: Fix build of deltachat-rpc-server-x86_64-darwin.
- Update rand to 0.9.
- Do not install `pdbpp` in the test environment for CFFI Python bindings.
- Migrate from tokio-tar to astral-tokio-tar.
- deps: Bump actions/setup-node from 5 to 6.
- deps: Bump cachix/install-nix-action from 31.8.0 to 31.8.1.
- Fix Rust 1.91.0 lint for derivable Default.
### CI
- Pin GitHub action `astral-sh/setup-uv`.
- Set 7 days cooldown on Dependabot updates.
- Update Rust to 1.91.0.
### Documentation
- Document Autocrypt-Gossip `_verified` attribute.
### Features/Changes
Metadata reduction:
- Protect Autocrypt header.
- Anonymize OpenPGP recipients (temorarily disabled due to interoperability problems, see <https://github.com/chatmail/core/issues/7384>).
- Protect the `Date` header.
Onboarding improvements:
- Allow plain domain in `dcaccount:` scheme.
- Do not resolve MX records during configuration.
Preparation for multi-transport:
- Move the messages only from INBOX and Spam folders.
- deltachat-rpc-client: Support multiple transports in resetup_account().
Various other changes:
- Opt-in weekly sending of statistics ([#6851](https://github.com/chatmail/core/pull/6851))
- Synchronize encrypted groups creation across devices ([#7001](https://github.com/chatmail/core/pull/7001)).
- Do not send Autocrypt in MDNs.
- Do not run SecureJoin if we are already in the group.
- Show if proxy is enabled in connectivity view ([#7359](https://github.com/chatmail/core/pull/7359)).
### Fixes
- Don't ignore QR token timestamp from sync messages.
- Do not allow sync item timestamps to be in the future.
- jsonrpc: Fix `ChatListItem::is_self_in_group`.
- Delete obsolete "configured*" keys from `config` table ([#7171](https://github.com/chatmail/core/pull/7171)).
- Fix flaky tests::verified_chats::test_verified_chat_editor_reordering and receive_imf::receive_imf_tests::test_two_group_securejoins.
- Stop using `leftgrps` table.
- Stop notifying about messages in contact request chats.
### Refactor
- Remove invalid Gmail OAuth2 tokens.
- Remove ProtectionStatus.
- Rename chat::create_group_chat() to create_group().
- Remove error stock strings that are rarely used these days ([#7327](https://github.com/chatmail/core/pull/7327)).
- Jsonrpc rename change casing in names of jsonrpc structs/enums to comply with rust naming conventions. ([#7324](https://github.com/chatmail/core/pull/7324)).
- Stop using deprecated Account.configure().
- add_transport_from_qr: Do not set deprecated config values.
- sql: Change second query_map function from FnMut to FnOnce.
- sql: Add query_map_vec().
- sql: Add query_map_collect().
- Use rand::fill() instead of rand::rng().fill().
- Use SampleString.
- Remove unused call to get_credentials().
### Tests
- rpc-client: VCard color is the same as the contact color ([#7294](https://github.com/chatmail/core/pull/7294)).
- Add unique offsets to ids generated by `TestContext` to increase test correctness ([#7297](https://github.com/chatmail/core/pull/7297)).
## [2.22.0] - 2025-10-17
### Fixes
- Do not notify about incoming calls for contact requests and blocked contacts.
### Tests
- Accept the chat with the caller before accepting calls.
## [2.21.0] - 2025-10-16
### Build system
- nix: Remove unused dependencies.
### Features / Changes
- TLS 1.3 session resumption.
- REPL: Add send-sync command.
- Set `User-Agent` for tile.openstreetmap.org requests.
- Cache tile.openstreetmap.org tiles for 7 days.
### Fixes
- Remove Exif with non-fatal errors from images.
- jsonrpc: Use Core's logic for computing VcardContact.color ([#7294](https://github.com/chatmail/core/pull/7294)).
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.7.0 to 31.8.0.
- cargo: Bump async_zip from 0.0.17 to 0.0.18 ([#7257](https://github.com/chatmail/core/pull/7257)).
- deps: Bump github/codeql-action from 3 to 4 ([#7304](https://github.com/chatmail/core/pull/7304)).
### Refactor
- Use rustls reexported from tokio_rustls.
- Pass ALPN around as &str.
- mimeparser: Store only one signature fingerprint.
### Tests
- Test expiration of ephemeral messages with unknown viewtype.
- Test expiration of non-ephemeral message with unknown viewtype.
## [2.20.0] - 2025-10-13
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
@@ -7096,8 +6921,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0
[2.21.0]: https://github.com/chatmail/core/compare/v2.20.0..v2.21.0
[2.22.0]: https://github.com/chatmail/core/compare/v2.21.0..v2.22.0
[2.23.0]: https://github.com/chatmail/core/compare/v2.22.0..v2.23.0
[2.24.0]: https://github.com/chatmail/core/compare/v2.23.0..v2.24.0
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0

82
Cargo.lock generated
View File

@@ -198,21 +198,6 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "astral-tokio-tar"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
dependencies = [
"filetime",
"futures-core",
"libc",
"portable-atomic",
"rustc-hash",
"tokio",
"tokio-stream",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -361,15 +346,15 @@ dependencies = [
[[package]]
name = "async_zip"
version = "0.0.18"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.17",
"thiserror 1.0.69",
"tokio",
"tokio-util",
]
@@ -1304,10 +1289,9 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.25.0"
version = "2.20.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
"async-broadcast",
"async-channel 2.5.0",
"async-imap",
@@ -1332,6 +1316,7 @@ dependencies = [
"futures",
"futures-lite",
"hex",
"hickory-resolver",
"http-body-util",
"humansize",
"hyper",
@@ -1358,10 +1343,10 @@ dependencies = [
"qrcodegen",
"quick-xml",
"rand 0.8.5",
"rand 0.9.0",
"ratelimit",
"regex",
"rusqlite",
"rustls",
"rustls-pki-types",
"sanitize-filename",
"sdp",
@@ -1383,6 +1368,7 @@ dependencies = [
"tokio-io-timeout",
"tokio-rustls",
"tokio-stream",
"tokio-tar",
"tokio-util",
"toml",
"tracing",
@@ -1413,7 +1399,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.25.0"
version = "2.20.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1435,7 +1421,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.25.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1451,7 +1437,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.25.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1480,7 +1466,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.25.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1488,7 +1474,7 @@ dependencies = [
"human-panic",
"libc",
"num-traits",
"rand 0.9.0",
"rand 0.8.5",
"serde_json",
"thiserror 2.0.17",
"tokio",
@@ -2029,14 +2015,14 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.25"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
[[package]]
@@ -3285,7 +3271,6 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.1",
"libc",
"redox_syscall 0.5.12",
]
[[package]]
@@ -4875,6 +4860,15 @@ dependencies = [
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -6156,6 +6150,21 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "tokio-tar"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75"
dependencies = [
"filetime",
"futures-core",
"libc",
"redox_syscall 0.3.5",
"tokio",
"tokio-stream",
"xattr",
]
[[package]]
name = "tokio-tfo"
version = "0.3.1"
@@ -7268,6 +7277,17 @@ dependencies = [
"time",
]
[[package]]
name = "xattr"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
dependencies = [
"libc",
"linux-raw-sys 0.4.14",
"rustix 0.38.44",
]
[[package]]
name = "xml-rs"
version = "0.8.25"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.25.0"
version = "2.20.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -47,7 +47,7 @@ async-channel = { workspace = true }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
@@ -61,6 +61,7 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
@@ -82,11 +83,11 @@ pgp = { version = "0.17.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.38", features = ["escape-html"] }
rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
sdp = "0.8.0"
serde_json = { workspace = true }
@@ -104,7 +105,7 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.5.6", default-features = false }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
@@ -156,11 +157,6 @@ name = "receive_emails"
required-features = ["internals"]
harness = false
[[bench]]
name = "decrypting"
required-features = ["internals"]
harness = false
[[bench]]
name = "get_chat_msgs"
harness = false
@@ -192,7 +188,7 @@ log = "0.4"
mailparse = "0.16.1"
nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.9"
rand = "0.8"
regex = "1.10"
rusqlite = "0.36"
sanitize-filename = "0.5"

View File

@@ -197,10 +197,12 @@ and then run the script.
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
@@ -213,3 +215,5 @@ or its language bindings:
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -1,200 +0,0 @@
//! Benchmarks for message decryption,
//! comparing decryption of symmetrically-encrypted messages
//! to decryption of asymmetrically-encrypted messages.
//!
//! Call with
//!
//! ```text
//! cargo bench --bench decrypting --features="internals"
//! ```
//!
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
//! ```
//!
//! You can also pass a substring.
//! So, you can run all 'Decrypt and parse' benchmarks with:
//!
//! ```text
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
//! ```
//!
//! Symmetric decryption has to try out all known secrets,
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::internals_for_benches::create_broadcast_secret;
use deltachat::internals_for_benches::create_dummy_keypair;
use deltachat::internals_for_benches::save_broadcast_secret;
use deltachat::{
Events,
chat::ChatId,
config::Config,
context::Context,
internals_for_benches::key_from_asc,
internals_for_benches::parse_and_get_text,
internals_for_benches::store_self_keypair,
pgp::{KeyPair, decrypt, pk_encrypt, symm_encrypt_message},
stock_str::StockStrings,
};
use rand::{Rng, rng};
use tempfile::tempdir;
const NUM_SECRETS: usize = 500;
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
.await
.unwrap();
context
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
.await
.unwrap();
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.signed_public_key();
let key_pair = KeyPair { public, secret };
store_self_keypair(&context, &key_pair)
.await
.expect("Failed to save key");
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Decrypt");
// ===========================================================================================
// Benchmarks for decryption only, without any other parsing
// ===========================================================================================
group.sample_size(10);
group.bench_function("Decrypt a symmetrically encrypted message", |b| {
let plain = generate_plaintext();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
let secret = secrets[NUM_SECRETS / 2].clone();
symm_encrypt_message(
plain.clone(),
create_dummy_keypair("alice@example.org").unwrap().secret,
black_box(&secret),
true,
)
.await
.unwrap()
});
b.iter(|| {
let mut msg =
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
let decrypted = msg.as_data_vec().unwrap();
assert_eq!(black_box(decrypted), plain);
});
});
group.bench_function("Decrypt a public-key encrypted message", |b| {
let plain = generate_plaintext();
let key_pair = create_dummy_keypair("alice@example.org").unwrap();
let secrets = generate_secrets();
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
pk_encrypt(
plain.clone(),
vec![black_box(key_pair.public.clone())],
Some(key_pair.secret.clone()),
true,
true,
)
.await
.unwrap()
});
b.iter(|| {
let mut msg = decrypt(
encrypted.clone().into_bytes(),
std::slice::from_ref(&key_pair.secret),
black_box(&secrets),
)
.unwrap();
let decrypted = msg.as_data_vec().unwrap();
assert_eq!(black_box(decrypted), plain);
});
});
// ===========================================================================================
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
// ===========================================================================================
let rt = tokio::runtime::Runtime::new().unwrap();
let mut secrets = generate_secrets();
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
// Put it into the middle of our secrets:
secrets[NUM_SECRETS / 2] = "secret".to_string();
let context = rt.block_on(async {
let context = create_context().await;
for (i, secret) in secrets.iter().enumerate() {
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
.await
.unwrap();
}
context
});
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "Symmetrically encrypted message");
}
});
});
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
let text = parse_and_get_text(
&ctx,
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
)
.await
.unwrap();
assert_eq!(text, "hi");
}
});
});
group.finish();
}
fn generate_secrets() -> Vec<String> {
let secrets: Vec<String> = (0..NUM_SECRETS)
.map(|_| create_broadcast_secret())
.collect();
secrets
}
fn generate_plaintext() -> Vec<u8> {
let mut plain: Vec<u8> = vec![0; 500];
rng().fill(&mut plain[..]);
plain
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.25.0"
version = "2.20.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -1763,7 +1763,9 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object.
* @param protect Deprecated 2025-08-31, ignored.
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
* @param name The name of the group chat to create.
* The name may be changed later using dc_set_chat_name().
* To find out the name of a group later, see dc_chat_get_name()
@@ -2563,7 +2565,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
#define DC_QR_ASK_VERIFYBROADCAST 204 // text1=broadcast name
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
@@ -2596,9 +2597,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
* with dc_lot_t::text1=Group name:
* ask whether to join the chat;
* - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name:
* ask whether to join the group;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
@@ -2681,8 +2681,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* 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
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* The returned text will also work as a normal https:-link,
@@ -2723,7 +2722,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
* 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
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
@@ -3891,12 +3890,18 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Deprecated, always returns 0.
* Check if a chat is protected.
*
* Only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Always 0.
* @deprecated 2025-09-09
* @return 1=chat protected, 0=chat is not protected.
*/
int dc_chat_is_protected (const dc_chat_t* chat);
@@ -5345,9 +5350,11 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
/**
* Create a provider struct for the given e-mail address by local lookup.
* Create a provider struct for the given e-mail address by local and DNS lookup.
*
* DNS lookup is not used anymore and this function is deprecated.
* First lookup is done from the local database as of dc_provider_new_from_email().
* If the first lookup fails, an additional DNS lookup is done,
* trying to figure out the provider belonging to custom domains.
*
* @memberof dc_provider_t
* @param context The context object.
@@ -5355,7 +5362,6 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
*/
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
@@ -6959,6 +6965,11 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_ENCR_NONE 28
/// "This message was encrypted for another setup."
///
/// Used as message text if decryption fails.
#define DC_STR_CANTDECRYPT_MSG_BODY 29
/// "Fingerprints"
///
/// Used to build the string returned by dc_get_contact_encrinfo().
@@ -7676,6 +7687,12 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/// "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
///
/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching
/// existing messages). But no more than once a day.
#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175
/// "You reacted %1$s to '%2$s'"
///
/// `%1$s` will be replaced by the reaction, usually an emoji
@@ -7732,22 +7749,17 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
/// "Security"
///
/// Used in connectivity view.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 201
/// "Scan to join channel %1$s"
///
/// Subtitle for channel join qrcode svg image generated by the core.
///
/// `%1$s` will be replaced with the channel name.
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
/// "Proxy Enabled"
///
/// Title for proxy section in connectivity view.
#define DC_STR_PROXY_ENABLED 220
/// "You are using a proxy. If you're having trouble connecting, try a different proxy."
///
/// Description in connectivity view when proxy is enabled.
#define DC_STR_PROXY_ENABLED_DESCRIPTION 221
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 202
/**
* @}

View File

@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock};
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::{Context, ContextBuilder};
@@ -39,6 +39,7 @@ use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use message::Viewtype;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
@@ -100,7 +101,7 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::random();
let id = rand::thread_rng().gen();
block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
@@ -128,7 +129,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
return ptr::null_mut();
}
let id = rand::random();
let id = rand::thread_rng().gen();
match block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
@@ -1720,7 +1721,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
#[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t,
_protect: libc::c_int,
protect: libc::c_int,
name: *const libc::c_char,
) -> u32 {
if context.is_null() || name.is_null() {
@@ -1728,12 +1729,22 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
block_on(chat::create_group(ctx, &to_string_lossy(name)))
.context("Failed to create group chat")
let Some(protect) = ProtectionStatus::from_i32(protect)
.context("Bad protect-value for dc_create_group_chat()")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
.ok()
else {
return 0;
};
block_on(async move {
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.context("Failed to create group chat")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
})
}
#[no_mangle]
@@ -3195,8 +3206,13 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
}
#[no_mangle]
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
0
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protected()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
@@ -4645,9 +4661,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
@@ -4666,13 +4686,25 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
match proxy_enabled {
Ok(proxy_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
proxy_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}
Err(_) => ptr::null_mut(),
}
}

View File

@@ -45,7 +45,6 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
@@ -99,7 +98,6 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
Qr::FprOk { .. } => LotState::QrFprOk,
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
@@ -126,7 +124,6 @@ impl Lot {
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::AskJoinBroadcast { .. } => Default::default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
@@ -169,9 +166,6 @@ pub enum LotState {
/// text1=groupname
QrAskVerifyGroup = 202,
/// text1=broadcast_name
QrAskJoinBroadcast = 204,
/// id=contact
QrFprOk = 210,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.25.0"
version = "2.20.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -12,6 +12,7 @@ use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
@@ -53,21 +54,20 @@ use types::contact::{ContactObject, VcardContact};
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::notify_state::JsonrpcNotifyState;
use types::provider_info::ProviderInfo;
use types::reactions::JsonrpcReactions;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::{MessageInfo, MessageLoadResult};
use self::types::{
chat::{BasicChat, JsonrpcChatVisibility, MuteDuration},
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
message::{
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
use crate::api::types::qr::QrObject;
#[derive(Debug)]
struct AccountState {
@@ -313,12 +313,6 @@ impl CommandApi {
}
}
/// Get the current push notification state.
async fn get_push_state(&self, account_id: u32) -> Result<JsonrpcNotifyState> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.push_state().await.into())
}
/// Get the combined filesize of an account in bytes
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
@@ -342,10 +336,21 @@ impl CommandApi {
/// instead of the domain.
async fn get_provider_info(
&self,
_account_id: u32,
account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
let ctx = self.get_context(account_id).await?;
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info = get_provider_info(
&ctx,
email.split('@').next_back().unwrap_or(""),
proxy_enabled,
)
.await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -388,6 +393,11 @@ impl CommandApi {
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -886,38 +896,6 @@ impl CommandApi {
Ok(chat_id.to_u32())
}
/// Like `secure_join()`, but allows to pass a source and a UI-path.
/// You only need this if your UI has an option to send statistics
/// to Delta Chat's developers.
///
/// **source**: The source where the QR code came from.
/// E.g. a link that was clicked inside or outside Delta Chat,
/// the "Paste from Clipboard" action,
/// the "Load QR code as image" action,
/// or a QR code scan.
///
/// **uipath**: Which UI path did the user use to arrive at the QR code screen.
/// If the SecurejoinSource was ExternalLink or InternalLink,
/// pass `None` here, because the QR code screen wasn't even opened.
/// ```
async fn secure_join_with_ux_info(
&self,
account_id: u32,
qr: String,
source: Option<SecurejoinSource>,
uipath: Option<SecurejoinUiPath>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let chat_id = securejoin::join_securejoin_with_ux_info(
&ctx,
&qr,
source.map(Into::into),
uipath.map(Into::into),
)
.await?;
Ok(chat_id.to_u32())
}
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
@@ -1001,16 +979,17 @@ impl CommandApi {
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
/// This may be useful if you want to show some help for just created groups.
///
/// `protect` argument is deprecated as of 2025-10-22 and is left for compatibility.
/// Pass `false` here.
async fn create_group_chat(
&self,
account_id: u32,
name: String,
_protect: bool,
) -> Result<u32> {
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
@@ -1019,7 +998,7 @@ impl CommandApi {
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_unencrypted(&ctx, &name)
chat::create_group_ex(&ctx, None, &name)
.await
.map(|id| id.to_u32())
}
@@ -1030,7 +1009,7 @@ impl CommandApi {
.await
}
/// Create a new, outgoing **broadcast channel**
/// Create a new **broadcast channel**
/// (called "Channel" in the UI).
///
/// Broadcast channels are similar to groups on the sending device,
@@ -1091,7 +1070,7 @@ impl CommandApi {
&self,
account_id: u32,
chat_id: u32,
visibility: JsonrpcChatVisibility,
visibility: JSONRPCChatVisibility,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1296,7 +1275,7 @@ impl CommandApi {
chat_id: u32,
info_only: bool,
add_daymarker: bool,
) -> Result<Vec<JsonrpcMessageListItem>> {
) -> Result<Vec<JSONRPCMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
@@ -1310,7 +1289,7 @@ impl CommandApi {
Ok(msg
.iter()
.map(|chat_item| (*chat_item).into())
.collect::<Vec<JsonrpcMessageListItem>>())
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
@@ -2231,7 +2210,7 @@ impl CommandApi {
&self,
account_id: u32,
message_id: u32,
) -> Result<Option<JsonrpcReactions>> {
) -> Result<Option<JSONRPCReactions>> {
let ctx = self.get_context(account_id).await?;
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
if reactions.is_empty() {

View File

@@ -6,6 +6,7 @@ use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
@@ -18,6 +19,18 @@ pub struct FullChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// Only verified contacts
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
/// can be added to protected chats.
///
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
/// by setting the 'protect' parameter to true.
///
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -45,7 +58,7 @@ pub struct FullChat {
archived: bool,
pinned: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: JsonrpcChatType,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
contacts: Vec<ContactObject>,
@@ -60,13 +73,6 @@ pub struct FullChat {
is_contact_request: bool,
is_device_chat: bool,
/// Note that this is different from
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
/// This property should only be accessed
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
//
// We could utilize [`Chat::is_self_in_chat`],
// but that would be an extra DB query.
self_in_group: bool,
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
@@ -125,11 +131,12 @@ impl FullChat {
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().into(),
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
@@ -165,6 +172,18 @@ pub struct BasicChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -191,7 +210,7 @@ pub struct BasicChat {
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
chat_type: JsonrpcChatType,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
color: String,
@@ -215,11 +234,12 @@ impl BasicChat {
Ok(BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().into(),
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,
@@ -258,52 +278,18 @@ impl MuteDuration {
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "ChatVisibility")]
pub enum JsonrpcChatVisibility {
pub enum JSONRPCChatVisibility {
Normal,
Archived,
Pinned,
}
impl JsonrpcChatVisibility {
impl JSONRPCChatVisibility {
pub fn into_core_type(self) -> ChatVisibility {
match self {
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
#[serde(rename = "ChatType")]
pub enum JsonrpcChatType {
Single,
Group,
Mailinglist,
OutBroadcast,
InBroadcast,
}
impl From<Chattype> for JsonrpcChatType {
fn from(chattype: Chattype) -> Self {
match chattype {
Chattype::Single => JsonrpcChatType::Single,
Chattype::Group => JsonrpcChatType::Group,
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
}
}
}
impl From<JsonrpcChatType> for Chattype {
fn from(chattype: JsonrpcChatType) -> Self {
match chattype {
JsonrpcChatType::Single => Chattype::Single,
JsonrpcChatType::Group => Chattype::Group,
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
}
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
use deltachat::chat::{Chat, ChatId};
use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::Contact;
use deltachat::contact::{Contact, ContactId};
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
@@ -11,7 +11,6 @@ use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
@@ -24,13 +23,14 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: JsonrpcChatType,
chat_type: u32,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
@@ -127,8 +127,11 @@ pub(crate) async fn get_chat_list_item_by_id(
None => (None, None),
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let contact = chat_contacts.first();
let was_seen_recently = match contact {
Some(contact) => Contact::get_by_id(ctx, *contact)
@@ -152,18 +155,19 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().into(),
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
is_device_talk: chat.is_device_talk(),
is_self_in_group: chat.is_self_in_chat(ctx).await?,
is_self_in_group: self_in_group,
is_sending_location: chat.is_sending_locations(),
is_archived: visibility == ChatVisibility::Archived,
is_pinned: visibility == ChatVisibility::Pinned,

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use deltachat::key::{DcKey, SignedPublicKey};
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -130,13 +130,7 @@ pub struct VcardContact {
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let is_self = false;
let fpr = vc.key.as_deref().and_then(|k| {
SignedPublicKey::from_base64(k)
.ok()
.map(|k| k.dc_fingerprint())
});
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,

View File

@@ -1,9 +1,8 @@
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use num_traits::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Event {
@@ -308,7 +307,7 @@ pub enum EventType {
/// The type of the joined chat.
/// This can take the same values
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
chat_type: JsonrpcChatType,
chat_type: u32,
/// ID of the chat in case of success.
chat_id: u32,
@@ -571,7 +570,7 @@ impl From<CoreEventType> for EventType {
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
chat_type: chat_type.into(),
chat_type: chat_type.to_u32().unwrap_or(0),
chat_id: chat_id.to_u32(),
progress,
},

View File

@@ -16,10 +16,9 @@ use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::chat::JsonrpcChatType;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JsonrpcReactions;
use super::reactions::JSONRPCReactions;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
@@ -103,7 +102,7 @@ pub struct MessageObject {
saved_message_id: Option<u32>,
reactions: Option<JsonrpcReactions>,
reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
}
@@ -532,7 +531,8 @@ pub struct MessageSearchResult {
chat_profile_image: Option<String>,
chat_color: String,
chat_name: String,
chat_type: JsonrpcChatType,
chat_type: u32,
is_chat_protected: bool,
is_chat_contact_request: bool,
is_chat_archived: bool,
message: String,
@@ -570,8 +570,9 @@ impl MessageSearchResult {
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),
chat_color,
chat_type: chat.get_type().into(),
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),
@@ -582,7 +583,7 @@ impl MessageSearchResult {
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JsonrpcMessageListItem {
pub enum JSONRPCMessageListItem {
Message {
msg_id: u32,
},
@@ -595,13 +596,13 @@ pub enum JsonrpcMessageListItem {
},
}
impl From<ChatItem> for JsonrpcMessageListItem {
impl From<ChatItem> for JSONRPCMessageListItem {
fn from(item: ChatItem) -> Self {
match item {
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
msg_id: msg_id.to_u32(),
},
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
}
}
}

View File

@@ -8,7 +8,6 @@ pub mod http;
pub mod location;
pub mod login_param;
pub mod message;
pub mod notify_state;
pub mod provider_info;
pub mod qr;
pub mod reactions;

View File

@@ -1,26 +0,0 @@
use deltachat::push::NotifyState;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "NotifyState")]
pub enum JsonrpcNotifyState {
/// Not subscribed to push notifications.
NotConnected,
/// Subscribed to heartbeat push notifications.
Heartbeat,
/// Subscribed to push notifications for new messages.
Connected,
}
impl From<NotifyState> for JsonrpcNotifyState {
fn from(state: NotifyState) -> Self {
match state {
NotifyState::NotConnected => Self::NotConnected,
NotifyState::Heartbeat => Self::Heartbeat,
NotifyState::Connected => Self::Connected,
}
}
}

View File

@@ -1,5 +1,4 @@
use deltachat::qr::Qr;
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -35,26 +34,6 @@ pub enum QrObject {
/// Authentication code.
authcode: String,
},
/// Ask the user whether to join the broadcast channel.
AskJoinBroadcast {
/// The user-visible name of this broadcast channel
name: String,
/// A string of random characters,
/// uniquely identifying this broadcast channel across all databases/clients.
/// Called `grpid` for historic reasons:
/// The id of multi-user chats is always called `grpid` in the database
/// because groups were once the only multi-user chats.
grpid: String,
/// ID of the contact who owns the broadcast channel and created the QR code.
contact_id: u32,
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
fingerprint: String,
/// Invite number.
invitenumber: String,
/// Authentication code.
authcode: String,
},
/// Contact fingerprint is verified.
///
/// Ask the user if they want to start chatting.
@@ -228,25 +207,6 @@ impl From<Qr> for QrObject {
authcode,
}
}
Qr::AskJoinBroadcast {
name,
grpid,
contact_id,
fingerprint,
authcode,
invitenumber,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskJoinBroadcast {
name,
grpid,
contact_id,
fingerprint,
authcode,
invitenumber,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
@@ -344,53 +304,3 @@ impl From<Qr> for QrObject {
}
}
}
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
pub enum SecurejoinSource {
/// Because of some problem, it is unknown where the QR code came from.
Unknown,
/// The user opened a link somewhere outside Delta Chat
ExternalLink,
/// The user clicked on a link in a message inside Delta Chat
InternalLink,
/// The user clicked "Paste from Clipboard" in the QR scan activity
Clipboard,
/// The user clicked "Load QR code as image" in the QR scan activity
ImageLoaded,
/// The user scanned a QR code
Scan,
}
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
pub enum SecurejoinUiPath {
/// The UI path is unknown, or the user didn't open the QR code screen at all.
Unknown,
/// The user directly clicked on the QR icon in the main screen
QrIcon,
/// The user first clicked on the `+` button in the main screen,
/// and then on "New Contact"
NewContact,
}
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
fn from(value: SecurejoinSource) -> Self {
match value {
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
}
}
}
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
fn from(value: SecurejoinUiPath) -> Self {
match value {
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
}
}
}

View File

@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
/// A single reaction emoji.
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Reaction", rename_all = "camelCase")]
pub struct JsonrpcReaction {
pub struct JSONRPCReaction {
/// Emoji.
emoji: String,
@@ -22,14 +22,14 @@ pub struct JsonrpcReaction {
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JsonrpcReactions {
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order.
reactions: Vec<JsonrpcReaction>,
reactions: Vec<JSONRPCReaction>,
}
impl From<Reactions> for JsonrpcReactions {
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
@@ -56,7 +56,7 @@ impl From<Reactions> for JsonrpcReactions {
false
};
let reaction = JsonrpcReaction {
let reaction = JSONRPCReaction {
emoji,
count,
is_from_self,
@@ -64,7 +64,7 @@ impl From<Reactions> for JsonrpcReactions {
reactions_v.push(reaction)
}
JsonrpcReactions {
JSONRPCReactions {
reactions_by_contact,
reactions: reactions_v,
}

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.25.0"
"version": "2.20.0"
}

View File

@@ -40,35 +40,15 @@ const constants = data
key.startsWith("DC_DOWNLOAD") ||
key.startsWith("DC_INFO_") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_") ||
key.startsWith("DC_CERTCK_") ||
key.startsWith("DC_SOCKET_") ||
key.startsWith("DC_LP_AUTH_") ||
key.startsWith("DC_PUSH_") ||
key.startsWith("DC_TEXT1_") ||
key.startsWith("DC_CHAT_TYPE")
key.startsWith("DC_QR_")
);
})
.map((row) => {
return ` export const ${row.key} = ${row.value};`;
return ` ${row.key}: ${row.value}`;
})
.join("\n");
.join(",\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!
export namespace C {
${constants}
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
export const DC_CHAT_TYPE_GROUP = "Group";
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
export const DC_CHAT_TYPE_SINGLE = "Single";
}\n`,
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.25.0"
version = "2.20.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -6,7 +6,9 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::{bail, ensure, Result};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -70,6 +72,11 @@ async fn reset_tables(context: &Context, bits: i32) {
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", ())
.await
.unwrap();
println!("(8) Rest but server config reset.");
}
@@ -340,6 +347,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -350,7 +358,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-sync <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
@@ -555,7 +562,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}",
"{}#{}: {} [{} fresh] {}{}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -566,6 +573,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
if chat.is_contact_request() {
"🆕"
} else {
@@ -680,7 +688,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}{}",
"{}#{}: {} [{}]{}{}{} {}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -698,6 +706,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -726,7 +739,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group(&context, arg1).await?;
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
println!("Group#{chat_id} created successfully.");
}
@@ -736,6 +750,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Broadcast#{chat_id} created successfully.");
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("Group#{chat_id} created and protected successfully.");
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
@@ -887,23 +908,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
"send-sync" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
// Send message over a dedicated SMTP connection
// and measure time.
//
// This can be used to benchmark SMTP connection establishment.
let time_start = std::time::Instant::now();
let msg = format!("{arg1} {arg2}");
let mut msg = Message::new_text(msg);
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
let time_needed = time_start.elapsed();
println!("Sent message in {time_needed:?}.");
}
"sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
@@ -1244,7 +1248,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
.await?;
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
@@ -199,7 +199,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"dellocations",
"getlocations",
"send",
"send-sync",
"sendempty",
"sendimage",
"sendsticker",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.25.0"
version = "2.20.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -125,11 +125,6 @@ class Account:
"""Add a new transport."""
yield self._rpc.add_or_update_transport.future(self.id, params)
@futuremethod
def add_transport_from_qr(self, qr: str):
"""Add a new transport using a QR code."""
yield self._rpc.add_transport_from_qr.future(self.id, qr)
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""
@@ -305,7 +300,7 @@ class Account:
chats.append(AttrDict(item))
return chats
def create_group(self, name: str) -> Chat:
def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation,
@@ -322,11 +317,15 @@ class Account:
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
(see `get_full_snapshot()` / `get_basic_snapshot()`).
This may be useful if you want to show some help for just created groups.
:param protect: If set to 1 the function creates group with protection initially enabled.
Only verified members are allowed in these groups
and end-to-end-encryption is always enabled.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
def create_broadcast(self, name: str) -> Chat:
"""Create a new, outgoing **broadcast channel**
"""Create a new **broadcast channel**
(called "Channel" in the UI).
Broadcast channels are similar to groups on the sending device,

View File

@@ -83,10 +83,11 @@ class Client:
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the client."""
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
self.account.set_config(key, value)
params = {"addr": email, "password": password}
self.account.add_or_update_transport(params)
self.account.configure()
self.logger.debug("Account configured")
def run_forever(self) -> None:

View File

@@ -91,17 +91,19 @@ class ChatId(IntEnum):
LAST_SPECIAL = 9
class ChatType(str, Enum):
class ChatType(IntEnum):
"""Chat type."""
SINGLE = "Single"
UNDEFINED = 0
SINGLE = 100
"""1:1 chat, i.e. a direct chat with a single contact"""
GROUP = "Group"
GROUP = 120
MAILINGLIST = "Mailinglist"
MAILINGLIST = 140
OUT_BROADCAST = "OutBroadcast"
OUT_BROADCAST = 160
"""Outgoing broadcast channel, called "Channel" in the UI.
The user can send into this channel,
@@ -113,7 +115,7 @@ class ChatType(str, Enum):
which would make it hard to grep for it.
"""
IN_BROADCAST = "InBroadcast"
IN_BROADCAST = 165
"""Incoming broadcast channel, called "Channel" in the UI.
This channel is read-only,

View File

@@ -93,17 +93,6 @@ class Message:
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break
def resend(self) -> None:
"""Resend messages and make information available for newly added chat members.
Resending sends out the original message, however, recipients and webxdc-status may differ.
Clients that already have the original message can still ignore the resent message as
they have tracked the state by dedicated updates.
Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
or messages that are not sent by SELF.
"""
self._rpc.resend_messages(self.account.id, [self.id])
@futuremethod
def send_webxdc_realtime_advertisement(self):
"""Send an advertisement to join the realtime channel."""

View File

@@ -43,9 +43,10 @@ class ACFactory:
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
params = {"addr": addr, "password": password}
yield account.add_or_update_transport.future(params)
assert account.is_configured()
return account
@@ -72,11 +73,11 @@ class ACFactory:
def resetup_account(self, ac: Account) -> Account:
"""Resetup account from scratch, losing the encryption key."""
ac.stop_io()
transports = ac.list_transports()
ac.remove()
ac_clone = self.get_unconfigured_account()
for transport in transports:
ac_clone.add_or_update_transport(transport)
for i in ["addr", "mail_pw"]:
ac_clone.set_config(i, ac.get_config(i))
ac.remove()
ac_clone.configure()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:

View File

@@ -9,7 +9,6 @@ def test_calls(acfactory) -> None:
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
@@ -68,7 +67,6 @@ a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call(place_call_info)
@@ -86,24 +84,3 @@ def test_ice_servers(acfactory) -> None:
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1
def test_no_contact_request_call(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.place_outgoing_call("offer")
alice_chat_bob.send_text("Hello!")
# Notification for "Hello!" message should arrive
# without the call ringing.
while True:
event = bob.wait_for_event()
# There should be no incoming call notification.
assert event.kind != EventType.INCOMING_CALL
if event.kind == EventType.MSGS_CHANGED:
msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!":
break

View File

@@ -169,8 +169,6 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
bob.create_chat(alice)
alice_chat_bob.send_text("hello")
msg = bob.wait_for_incoming_msg()

View File

@@ -214,9 +214,7 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot()
assert ac2_webxdc_msg_snapshot.text == "WebXDC"
ac2_webxdc_msg_snapshot.chat.accept()
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
ac1_ac2_chat.send_text("Hello!")
ac2_hello_msg = ac2.wait_for_incoming_msg()

View File

@@ -18,7 +18,9 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
alice1 = acfactory.get_online_account()
alice2 = acfactory.get_unconfigured_account()
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.bring_online()
setup_code = alice1.initiate_autocrypt_key_transfer()
@@ -35,7 +37,9 @@ def test_ac_setup_message_twice(acfactory):
alice1 = acfactory.get_online_account()
alice2 = acfactory.get_unconfigured_account()
alice2.add_or_update_transport({"addr": alice1.get_config("addr"), "password": alice1.get_config("mail_pw")})
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.bring_online()
# Send the first Autocrypt Setup Message and ignore it.

View File

@@ -3,7 +3,6 @@ import logging
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
from deltachat_rpc_client.const import ChatType
from deltachat_rpc_client.rpc import JsonRpcError
@@ -59,7 +58,8 @@ def test_qr_setup_contact_svg(acfactory) -> None:
assert "Alice" in svg
def test_qr_securejoin(acfactory):
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
@@ -67,7 +67,8 @@ def test_qr_securejoin(acfactory):
alice2 = alice.clone()
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
alice_chat = alice.create_group("Group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
@@ -88,6 +89,7 @@ def test_qr_securejoin(acfactory):
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
@@ -110,143 +112,6 @@ def test_qr_securejoin(acfactory):
fiona.wait_for_securejoin_joiner_success()
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
alice, bob, fiona = acfactory.get_online_accounts(3)
alice2 = alice.clone()
bob2 = bob.clone()
if all_devices_online:
alice2.start_io()
bob2.start_io()
logging.info("===================== Alice creates a broadcast =====================")
alice_chat = alice.create_broadcast("Broadcast channel!")
snapshot = alice_chat.get_basic_snapshot()
assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
logging.info("===================== Bob joins the broadcast =====================")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
alice_chat.send_text("Hello everyone!")
def get_broadcast(ac):
chat = ac.get_chatlist(query="Broadcast channel!")[0]
assert chat.get_basic_snapshot().name == "Broadcast channel!"
return chat
def wait_for_broadcast_messages(ac):
chat = get_broadcast(ac)
snapshot = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You joined the channel."
assert snapshot.chat_id == chat.id
snapshot = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello everyone!"
assert snapshot.chat_id == chat.id
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
# Check that the chat partner is verified.
contact_snapshot = contact.get_snapshot()
assert contact_snapshot.is_verified
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs[0].get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs[1].get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
hello_msg = chat_msgs[2].get_snapshot()
assert hello_msg.text == "Hello everyone!"
assert not hello_msg.is_info
assert hello_msg.show_padlock
assert hello_msg.error is None
assert len(chat_msgs) == 3
chat_snapshot = chat.get_full_snapshot()
assert chat_snapshot.is_encrypted
assert chat_snapshot.name == "Broadcast channel!"
if inviter_side:
assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
else:
assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert chat_snapshot.can_send == inviter_side
chat_contacts = chat_snapshot.contact_ids
assert contact.id in chat_contacts
if inviter_side:
assert len(chat_contacts) == 1
else:
assert len(chat_contacts) == 2
assert SpecialContactId.SELF in chat_contacts
assert chat_snapshot.self_in_group
wait_for_broadcast_messages(bob)
check_account(alice, alice.create_contact(bob), inviter_side=True)
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
logging.info("===================== Test Alice's second device =====================")
# Start second Alice device, if it wasn't started already.
alice2.start_io()
while True:
msg_id = alice2.wait_for_msgs_changed_event().msg_id
if msg_id:
snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
if snapshot.text == "Hello everyone!":
break
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
logging.info("===================== Test Bob's second device =====================")
# Start second Bob device, if it wasn't started already.
bob2.start_io()
bob2.wait_for_securejoin_joiner_success()
wait_for_broadcast_messages(bob2)
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
# The QR code token is synced, so alice2 must be able to handle join requests.
logging.info("===================== Fiona joins the group via alice2 =====================")
alice.stop_io()
fiona.secure_join(qr_code)
alice2.wait_for_securejoin_inviter_success()
fiona.wait_for_securejoin_joiner_success()
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "You joined the channel."
get_broadcast(alice2).get_messages()[2].resend()
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "Hello everyone!"
check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
# For Bob, the channel must not have changed:
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
@@ -260,8 +125,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
bob_chat_alice = snapshot.chat
assert bob_chat_alice.get_basic_snapshot().is_contact_request
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
@@ -285,8 +150,8 @@ def test_qr_readreceipt(acfactory) -> None:
for joiner in [bob, charlie]:
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a group")
group = alice.create_group("Group")
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
@@ -351,10 +216,11 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins the group")
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -417,10 +283,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
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
assert 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(acfactory):
@@ -438,8 +302,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
# 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")
ch1 = ac1.create_group("Group")
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -449,6 +313,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
@@ -462,7 +327,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.create_contact(ac2))
# ensure ac2 receives message in VG
@@ -470,6 +335,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
@@ -493,7 +359,7 @@ def test_qr_new_group_unblocked(acfactory):
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining")
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -518,7 +384,8 @@ def test_aeap_flow_verified(acfactory):
addr, password = acfactory.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
@@ -572,6 +439,7 @@ def test_gossip_verification(acfactory) -> None:
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
assert not bob_group_chat.get_basic_snapshot().is_protected
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")
@@ -580,14 +448,13 @@ def test_gossip_verification(acfactory) -> None:
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Group propagates verification using Autocrypt-Gossip header.
# Autocrypt group does not propagate 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
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
assert bob_group_chat.get_basic_snapshot().is_protected
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")
@@ -598,9 +465,7 @@ def test_gossip_verification(acfactory) -> None:
# 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
assert carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
@@ -612,7 +477,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Group")
ac3_chat = ac3.create_group("Verified group", protect=True)
# ac1 joins ac3 group.
ac3_qr_code = ac3_chat.get_qr_code()
@@ -660,6 +525,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.is_info
ac2_chat = snapshot.chat
assert ac2_chat.get_basic_snapshot().is_protected
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
@@ -669,8 +535,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
@@ -681,6 +548,7 @@ def test_withdraw_securejoin_qr(acfactory):
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()

View File

@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
from deltachat_rpc_client.rpc import JsonRpcError
@@ -338,27 +338,43 @@ def test_receive_imf_failure(acfactory) -> None:
bob.set_config("fail_on_receiving_full_msg", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == bob.get_device_chat().id
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert (
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.AVAILABLE
assert snapshot.error is not None
assert snapshot.show_padlock
# The failed message doesn't break the IMAP loop.
bob.set_config("fail_on_receiving_full_msg", "0")
alice_chat_bob.send_text("Hello again!")
event = bob.wait_for_incoming_msg_event()
assert event.chat_id == chat_id
msg_id = event.msg_id
message1 = bob.get_message_by_id(msg_id)
snapshot = message1.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.DONE
assert snapshot.error is None
# The failed message can be re-downloaded later.
bob._rpc.download_full_message(bob.id, message.id)
event = bob.wait_for_event(EventType.MSGS_CHANGED)
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.download_state == DownloadState.IN_PROGRESS
event = bob.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == chat_id
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Hello again!"
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.DONE
assert snapshot.error is None
assert snapshot.text == "Hello!"
def test_selfavatar_sync(acfactory, data, log) -> None:
@@ -554,11 +570,8 @@ def test_provider_info(rpc) -> None:
assert provider_info is None
# Test MX record resolution.
# This previously resulted in Gmail provider
# because MX record pointed to google.com domain,
# but MX record resolution has been removed.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
@@ -800,7 +813,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_account_settings
assert "cert_automatic" in alice.get_info().used_account_settings
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -872,12 +885,10 @@ def test_rename_group(acfactory):
bob_msg = bob.wait_for_incoming_msg()
bob_chat = bob_msg.get_snapshot().chat
assert bob_chat.get_basic_snapshot().name == "Test group"
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
for name in ["Baz", "Foo bar", "Xyzzy"]:
alice_group.set_name(name)
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
bob.wait_for_event(EventType.CHATLIST_ITEM_CHANGED)
bob.wait_for_incoming_msg_event()
assert bob_chat.get_basic_snapshot().name == name
@@ -913,103 +924,34 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
assert "DeltaChat" in ac1_direct_imap.list_folders()
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_leave_broadcast(acfactory, all_devices_online):
def test_broadcast(acfactory):
alice, bob = acfactory.get_online_accounts(2)
bob2 = bob.clone()
alice_chat = alice.create_broadcast("My great channel")
snapshot = alice_chat.get_basic_snapshot()
assert snapshot.name == "My great channel"
assert snapshot.is_unpromoted
assert snapshot.is_encrypted
assert snapshot.chat_type == ChatType.OUT_BROADCAST
if all_devices_online:
bob2.start_io()
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat.add_contact(alice_contact_bob)
logging.info("===================== Alice creates a broadcast =====================")
alice_chat = alice.create_broadcast("Broadcast channel!")
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
assert alice_msg.text == "hello"
assert alice_msg.show_padlock
logging.info("===================== Bob joins the broadcast =====================")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.text == "hello"
assert bob_msg.show_padlock
assert bob_msg.error is None
alice_bob_contact = alice.create_contact(bob)
alice_contacts = alice_chat.get_contacts()
assert len(alice_contacts) == 1 # 1 recipient
assert alice_contacts[0].id == alice_bob_contact.id
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
bob_chat_snapshot = bob_chat.get_basic_snapshot()
assert bob_chat_snapshot.name == "My great channel"
assert not bob_chat_snapshot.is_unpromoted
assert bob_chat_snapshot.is_encrypted
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert bob_chat_snapshot.is_contact_request
member_added_msg = bob.wait_for_incoming_msg()
assert member_added_msg.get_snapshot().text == "You joined the channel."
def get_broadcast(ac):
chat = ac.get_chatlist(query="Broadcast channel!")[0]
assert chat.get_basic_snapshot().name == "Broadcast channel!"
return chat
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
chat = get_broadcast(ac)
contact_snapshot = contact.get_snapshot()
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
if not inviter_side:
leave_msg = chat_msgs.pop(0).get_snapshot()
assert leave_msg.text == "You left the channel."
assert len(chat_msgs) == 0
chat_snapshot = chat.get_full_snapshot()
# On Alice's side, SELF is not in the list of contact ids
# because OutBroadcast chats never contain SELF in the list.
# On Bob's side, SELF is not in the list because he left.
if inviter_side:
assert len(chat_snapshot.contact_ids) == 0
else:
assert chat_snapshot.contact_ids == [contact.id]
logging.info("===================== Bob leaves the broadcast =====================")
bob_chat = get_broadcast(bob)
assert bob_chat.get_full_snapshot().self_in_group
assert len(bob_chat.get_contacts()) == 2 # Alice and Bob
bob_chat.leave()
assert not bob_chat.get_full_snapshot().self_in_group
# After Bob left, only Alice will be left in Bob's memberlist
assert len(bob_chat.get_contacts()) == 1
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
logging.info("===================== Test Alice's device =====================")
while len(alice_chat.get_contacts()) != 0: # After Bob left, there will be 0 recipients
alice.wait_for_event(EventType.CHAT_MODIFIED)
check_account(alice, alice.create_contact(bob), inviter_side=True)
logging.info("===================== Test Bob's second device =====================")
# Start second Bob device, if it wasn't started already.
bob2.start_io()
member_added_msg = bob2.wait_for_incoming_msg()
assert member_added_msg.get_snapshot().text == "You joined the channel."
bob2_chat = get_broadcast(bob2)
# After Bob left, only Alice will be left in Bob's memberlist
while len(bob2_chat.get_contacts()) != 1:
bob2.wait_for_event(EventType.CHAT_MODIFIED)
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
assert not bob_chat.can_send()

View File

@@ -1,12 +1,8 @@
def test_vcard(acfactory) -> None:
alice, bob, fiona = acfactory.get_online_accounts(3)
alice, bob = acfactory.get_online_accounts(2)
bob.create_chat(alice)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
@@ -16,12 +12,3 @@ def test_vcard(acfactory) -> None:
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
alice_chat_bob.send_contact(alice_contact_fiona)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.key
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.25.0"
version = "2.20.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.25.0"
"version": "2.20.0"
}

View File

@@ -20,11 +20,6 @@ impl SystemTimeTools {
pub fn shift(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
}
/// Simulates the system clock being rewound by `duration`.
pub fn shift_back(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() -= duration;
}
}
#[cfg(test)]

View File

@@ -36,6 +36,8 @@ skip = [
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "redox_syscall", version = "0.4.1" },
{ name = "rustix", version = "0.38.44" },
{ name = "serdect", version = "0.2.0" },
{ name = "spin", version = "0.9.8" },

View File

@@ -98,6 +98,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
@@ -237,9 +240,6 @@
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 = [
@@ -483,6 +483,12 @@
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.25.0"
version = "2.20.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -404,16 +404,18 @@ class Account:
self,
name: str,
contacts: Optional[List[Contact]] = None,
verified: bool = False,
) -> Chat:
"""create a new group chat object.
Chats are unpromoted until the first message is sent.
:param contacts: list of contacts to add
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
chat = Chat(self, chat_id)
if contacts is not None:
for contact in contacts:

View File

@@ -142,6 +142,13 @@ class Chat:
"""
return bool(lib.dc_chat_can_send(self._dc_chat))
def is_protected(self) -> bool:
"""return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return bool(lib.dc_chat_is_protected(self._dc_chat))
def get_name(self) -> Optional[str]:
"""return name of this chat.

View File

@@ -523,6 +523,7 @@ class ACFactory:
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
configdict.setdefault("delete_server_after", 0)
ac.update_config(configdict)
@@ -603,6 +604,20 @@ class ACFactory:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are end-to-end encrypted."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text
return chat
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):

View File

@@ -116,8 +116,10 @@ class TestGroupStressTests:
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1_addr = ac1.get_self_contact().addr
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -140,6 +142,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
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.chat.is_protected()
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
@@ -170,12 +173,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
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
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
@@ -267,7 +266,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -321,7 +321,8 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ac2.qr_join_chat(qr)
@@ -335,6 +336,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert msg_in.is_system_message()
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
@@ -374,7 +376,8 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -399,20 +402,29 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert not chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are end-to-end encrypted."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == 0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
# Until we reset verifications and then send the _verified header,
# verification is not gossiped here:
assert not ac2_offl_ac1_contact.is_verified()
assert msg_in.chat.is_protected()
assert ac2_offl_ac1_contact.is_verified()
def test_deleted_msgs_dont_reappear(acfactory):

View File

@@ -269,28 +269,26 @@ def test_enable_mvbox_move(acfactory, lp):
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
def test_mvbox_thread_and_trash(acfactory, lp):
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
lp.sec("ac2: start without a mvbox thread")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online()
lp.sec("ac1: create trash")
ac1.direct_imap.create_folder("Trash")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.stop_io()
ac1.start_io()
lp.sec("ac1: create and configure sentbox")
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_trash_folder") != "Trash":
while ac1.get_config("configured_sentbox_folder") != "Sent":
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -310,7 +308,7 @@ def test_move_works(acfactory):
def test_move_avoids_loop(acfactory):
"""Test that the message is only moved from INBOX to DeltaChat.
"""Test that the message is only moved once.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
@@ -321,14 +319,6 @@ def test_move_avoids_loop(acfactory):
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
# Create INBOX.DeltaChat folder and make sure
# it is detected by full folder scan.
ac2.direct_imap.create_folder("INBOX.DeltaChat")
ac2.stop_io()
ac2.start_io()
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
@@ -336,27 +326,19 @@ def test_move_avoids_loop(acfactory):
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX.DeltaChat again.
# We assume that test server uses "." as the delimiter.
# Move the message to the INBOX again.
ac2.direct_imap.select_folder("DeltaChat")
ac2.direct_imap.conn.move(["*"], "INBOX.DeltaChat")
ac2.direct_imap.conn.move(["*"], "INBOX")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg2.text == "Message 2"
# Stop and start I/O to trigger folder scan.
ac2.stop_io()
ac2.start_io()
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
# Check that Message 1 is still in the INBOX.DeltaChat folder
# Check that Message 1 is still in the INBOX folder
# and Message 2 is in the DeltaChat folder.
ac2.direct_imap.select_folder("INBOX")
assert len(ac2.direct_imap.get_all_messages()) == 0
ac2.direct_imap.select_folder("DeltaChat")
assert len(ac2.direct_imap.get_all_messages()) == 1
ac2.direct_imap.select_folder("INBOX.DeltaChat")
ac2.direct_imap.select_folder("DeltaChat")
assert len(ac2.direct_imap.get_all_messages()) == 1
@@ -460,7 +442,7 @@ def test_forward_own_message(acfactory, lp):
def test_resend_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat1 = acfactory.get_accepted_chat(ac1, ac2)
chat1 = ac1.create_chat(ac2)
lp.sec("ac1: send message to ac2")
chat1.send_text("message")
@@ -468,19 +450,14 @@ def test_resend_message(acfactory, lp):
lp.sec("ac2: receive message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
lp.sec("ac1: resend message")
ac1.resend_messages([msg_in])
lp.sec("ac1: send another message")
chat1.send_text("another message")
lp.sec("ac2: receive another message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "another message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
lp.sec("ac2: check that message is deleted")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
assert len(chat2.get_messages()) == chat2_msg_cnt
@@ -855,9 +832,9 @@ def test_no_draft_if_cant_send(acfactory):
def test_dont_show_emails(acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header, then ignore the email.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out and received later (i.e. it's in "Inbox"), it must be shown.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
ac1 = acfactory.new_online_configuring_account()
@@ -866,6 +843,7 @@ def test_dont_show_emails(acfactory, lp):
acfactory.wait_configured(ac1)
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
ac1.direct_imap.create_folder("Junk")
@@ -881,7 +859,21 @@ def test_dont_show_emails(acfactory, lp):
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts received later
message in Drafts that is moved to Sent later
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Sent",
"""
From: ac1 <{}>
Subject: subj
To: alice@example.org
Message-ID: <hsabaeni@example.org>
Content-Type: text/plain; charset=utf-8
message in Sent
""".format(
ac1.get_config("configured_addr"),
),
@@ -961,13 +953,14 @@ def test_dont_show_emails(acfactory, lp):
lp.sec("All prepared, now let DC find the message")
ac1.start_io()
msg = ac1._evtracker.wait_next_messages_changed()
# Wait until each folder was scanned, this is necessary for this test to test what it should test:
ac1._evtracker.wait_idle_inbox_ready()
fresh_msgs = list(ac1.get_fresh_messages())
msg = fresh_msgs[0]
assert msg.text == "subj message in Sent"
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 1
assert len(chat_msgs) == 2
assert any(msg.text == "subj Actually interesting message in Spam" for msg in chat_msgs)
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
@@ -975,16 +968,16 @@ def test_dont_show_emails(acfactory, lp):
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
lp.sec("'Send out' the draft by moving it to Inbox, and wait for DC to display it this time")
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Inbox")
ac1.direct_imap.conn.move(uid, "Sent")
ac1.start_io()
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts received later"
assert len(msg.chat.get_messages()) == 2
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 3
def test_bot(acfactory, lp):
@@ -1211,7 +1204,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification
that resulted in failure to propagate verification via gossip in a verified group
when the database already contained the contact with a different email address capitalization.
"""
@@ -1222,27 +1215,24 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
ac1.create_contact(ac2_addr_uppercase)
lp.sec("ac3 creates a group with a QR code")
chat = ac3.create_group_chat("hello")
lp.sec("ac3 creates a verified group with a QR code")
chat = ac3.create_group_chat("hello", verified=True)
qr = chat.get_join_qr()
lp.sec("ac1 joins a group via a QR code")
lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a group via a QR code")
lp.sec("ac2 joins a verified group via a QR code")
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
assert contact.is_verified()
def test_set_get_contact_avatar(acfactory, data, lp):
@@ -1767,10 +1757,10 @@ def test_group_quote(acfactory, lp):
"xyz",
), # Test that emails aren't found in a random folder
(
"xyz",
"Spam",
True,
"xyz",
), # ...emails are found in a random folder and downloaded without moving
"DeltaChat",
), # ...emails are moved from the spam folder to "DeltaChat"
(
"Spam",
False,

View File

@@ -271,9 +271,10 @@ class TestOfflineChat:
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
def test_group_chat_qr(self, acfactory, ac1):
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
chat = ac1.create_group_chat(name="title1", verified=verified)
assert chat.is_group()
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup

View File

@@ -23,6 +23,7 @@ deps =
pytest
pytest-timeout
pytest-xdist
pdbpp
requests
# urllib3 2.0 does not work in manylinux2014 containers.
# https://github.com/deltachat/deltachat-core-rust/issues/4788

View File

@@ -1 +1 @@
2025-11-05
2025-10-13

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.91.0
RUST_VERSION=1.90.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=d041136c19a48b493823b46d472f12b9ee94ae80
REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

20
spec.md
View File

@@ -1,6 +1,6 @@
# Chatmail Specification
Version: 0.37.0
Version: 0.36.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -582,24 +582,6 @@ 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.
# Transitioning to a new e-mail address (AEAP)
When receiving a message:

View File

@@ -61,11 +61,9 @@ 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;")?;
// }
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
@@ -284,9 +282,8 @@ mod tests {
.contains("test@example.com")
);
// We don't send the _verified header yet:
assert!(
!format!(
format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),

View File

@@ -468,7 +468,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;

View File

@@ -537,11 +537,7 @@ fn file_hash(src: &Path) -> Result<blake3::Hash> {
fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new()
.continue_on_error(true)
.read_from_container(&mut bufreader)
.or_else(|e| e.distill_partial_result(|_errors| {}))
.ok();
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}

View File

@@ -334,28 +334,6 @@ async fn test_recode_image_2() {
assert_correct_rotation(&img_rotated);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_bad_exif() {
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
// detected and removed.
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
SendImageCheckMediaquality {
viewtype: Viewtype::Image,
media_quality_config: "0",
bytes,
extension: "jpg",
has_exif: true,
original_width: 1000,
original_height: 1000,
compressed_width: 1000,
compressed_height: 1000,
..Default::default()
}
.test()
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../../test-data/image/screenshot.png");
@@ -440,7 +418,7 @@ async fn test_recode_image_balanced_png() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
let bytes = include_bytes!("../../test-data/image/logo.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
bytes,

View File

@@ -2,9 +2,8 @@
//!
//! Internally, calls are bound a user-visible message initializing the call.
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::{Blocked, Chattype};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
@@ -135,19 +134,8 @@ impl CallInfo {
}
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
let now = time();
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
self.msg.update_param(context).await?;
// Store ended timestamp in calls table. If no entry exists yet, create one.
context
.sql
.execute(
"INSERT INTO calls (msg_id, ended_timestamp) VALUES (?, ?)
ON CONFLICT(msg_id) DO UPDATE SET ended_timestamp=excluded.ended_timestamp",
(self.msg.id, now),
)
.await?;
Ok(())
}
@@ -163,16 +151,6 @@ impl CallInfo {
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
self.msg.update_param(context).await?;
// Store ended timestamp in calls table. If no entry exists yet, create one.
context
.sql
.execute(
"INSERT INTO calls (msg_id, ended_timestamp) VALUES (?, ?)
ON CONFLICT(msg_id) DO UPDATE SET ended_timestamp=excluded.ended_timestamp",
(self.msg.id, now),
)
.await?;
Ok(())
}
@@ -214,15 +192,11 @@ impl Context {
let mut call = Message {
viewtype: Viewtype::Call,
text: "Outgoing call".into(),
call_sdp_offer: Some(place_call_info.clone()),
..Default::default()
};
call.param.set(Param::WebrtcRoom, &place_call_info);
call.id = send_msg(self, chat_id, &mut call).await?;
// For outgoing calls, we don't store our own offer SDP in the database.
// It's only kept in memory for sending the message.
let wait = RINGING_SECONDS;
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
@@ -254,14 +228,6 @@ impl Context {
chat.id.accept(self).await?;
}
// Store our answer SDP in calls table (replacing the offer from the other side)
self.sql
.execute(
"UPDATE calls SET sdp=? WHERE msg_id=?",
(&accept_call_info, call_id),
)
.await?;
// send an acceptance message around: to the caller as well as to the other devices of the callee
let mut msg = Message {
viewtype: Viewtype::Text,
@@ -270,6 +236,8 @@ impl Context {
};
msg.param.set_cmd(SystemMessage::CallAccepted);
msg.hidden = true;
msg.param
.set(Param::WebrtcAccepted, accept_call_info.to_string());
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::IncomingCallAccepted {
@@ -358,16 +326,6 @@ impl Context {
from_id: ContactId,
) -> Result<()> {
if mime_message.is_call() {
// Extract SDP offer from message headers and store in calls table for incoming calls
if let Some(offer_sdp) = mime_message.get_header(HeaderDef::ChatWebrtcRoom) {
self.sql
.execute(
"INSERT OR IGNORE INTO calls (msg_id, sdp) VALUES (?, ?)",
(call_id, offer_sdp),
)
.await?;
}
let Some(call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
@@ -387,27 +345,12 @@ impl Context {
false
}
};
if let Some(chat_id_blocked) =
ChatIdBlocked::lookup_by_contact(self, from_id).await?
{
match chat_id_blocked.blocked {
Blocked::Not => {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
}
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
}
}
}
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
let wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
@@ -432,18 +375,6 @@ impl Context {
return Ok(());
}
// Store SDP answer in calls table for outgoing calls
// (for incoming calls, we've already replaced our offer with our answer in accept_incoming_call)
if let Some(answer_sdp) = mime_message.get_header(HeaderDef::ChatWebrtcAccepted)
{
self.sql
.execute(
"INSERT OR REPLACE INTO calls (msg_id, sdp) VALUES (?, ?)",
(call.msg.id, answer_sdp),
)
.await?;
}
call.mark_as_accepted(self).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming() {
@@ -516,49 +447,33 @@ impl Context {
/// not a call message, returns `None`.
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
let call = Message::load_from_db(self, call_id).await?;
self.load_call_by_message(call).await
Ok(self.load_call_by_message(call))
}
// Loads information about the call given the `Message`.
//
// If the `Message` is not a call message, returns `None`.
//
// This function is async because it queries the calls table
// to retrieve SDP offers and answers.
async fn load_call_by_message(&self, call: Message) -> Result<Option<CallInfo>> {
// If the `Message` is not a call message, returns `None`
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
if call.viewtype != Viewtype::Call {
// This can happen e.g. if a "call accepted"
// or "call ended" message is received
// with `In-Reply-To` referring to non-call message.
return Ok(None);
return None;
}
// Load SDP from calls table. Returns empty strings if no record exists,
// which can happen for old messages from before the migration or for
// calls where SDPs have been cleaned up by housekeeping.
// For incoming calls, the SDP is the offer from the other side.
// For outgoing calls (after acceptance), the SDP is the answer from the other side.
let sdp = self
.sql
.query_row_optional("SELECT sdp FROM calls WHERE msg_id=?", (call.id,), |row| {
row.get::<_, String>(0)
})
.await?
.unwrap_or_default();
let (place_call_info, accept_call_info) = if call.from_id == ContactId::SELF {
// Outgoing call: the stored SDP (if any) is the answer from the other side
(String::new(), sdp)
} else {
// Incoming call: the stored SDP is the offer from the other side
(sdp, String::new())
};
Ok(Some(CallInfo {
place_call_info,
accept_call_info,
Some(CallInfo {
place_call_info: call
.param
.get(Param::WebrtcRoom)
.unwrap_or_default()
.to_string(),
accept_call_info: call
.param
.get(Param::WebrtcAccepted)
.unwrap_or_default()
.to_string(),
msg: call,
}))
})
}
}

View File

@@ -4,8 +4,6 @@ use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::SystemTime;
use std::time::Duration;
struct CallSetup {
pub alice: TestContext,
@@ -47,12 +45,6 @@ async fn setup_call() -> Result<CallSetup> {
// Alice creates a chat with Bob and places an outgoing call there.
// Alice's other device sees the same message as an outgoing call.
let alice_chat = alice.create_chat(&bob).await;
// Create chat on Bob's side
// so incoming call causes a notification.
bob.create_chat(&alice).await;
bob2.create_chat(&alice).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.await?;
@@ -674,74 +666,3 @@ async fn test_no_partial_calls() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_housekeeping_deletes_old_call_sdps() -> Result<()> {
use crate::sql::housekeeping;
let alice = TestContext::new_alice().await;
// Simulate receiving an incoming call from Bob
let received_call = receive_imf(
&alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <incoming-call@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call\n\
Chat-Webrtc-Room: dGVzdC1zZHAtb2ZmZXI=\n\
\n\
Hello, this is a call\n",
false,
)
.await?
.unwrap();
let call_id = received_call.msg_ids[0];
// Verify SDP is stored in calls table for incoming call
let sdp_before: Option<String> = alice
.sql
.query_row_optional("SELECT sdp FROM calls WHERE msg_id=?", (call_id,), |row| {
row.get(0)
})
.await?;
assert!(sdp_before.is_some());
// End the call
alice.end_call(call_id).await?;
// Verify the call message is marked as ended
let call = alice.load_call_by_id(call_id).await?.unwrap();
assert!(call.is_ended());
// SDP should still be there after ending
let sdp_after_end: Option<String> = alice
.sql
.query_row_optional("SELECT sdp FROM calls WHERE msg_id=?", (call_id,), |row| {
row.get(0)
})
.await?;
assert!(sdp_after_end.is_some());
// Simulate passage of time - shift forward by 24 hours + 1 second
SystemTime::shift(Duration::from_secs(86400 + 1));
// Run housekeeping
housekeeping(&alice).await?;
// Verify SDP has been deleted from calls table
let sdp_after_housekeeping: Option<String> = alice
.sql
.query_row_optional("SELECT sdp FROM calls WHERE msg_id=?", (call_id,), |row| {
row.get(0)
})
.await?;
assert_eq!(sdp_after_housekeeping, None);
// The call message should still exist
let msg = Message::load_from_db(&alice, call_id).await?;
assert_eq!(msg.viewtype, Viewtype::Call);
Ok(())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -107,6 +107,11 @@ impl Chatlist {
Ok((chat_id, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
@@ -127,7 +132,7 @@ impl Chatlist {
// groups. Otherwise it would be hard to follow conversations.
let ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map_vec(
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -145,6 +150,7 @@ impl Chatlist {
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
} else if flag_archived_only {
// show archived chats
@@ -153,7 +159,7 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map_vec(
.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -171,6 +177,7 @@ impl Chatlist {
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft,),
process_row,
process_rows,
)
.await?
} else if let Some(query) = query {
@@ -188,7 +195,7 @@ impl Chatlist {
let str_like_cmd = format!("%{query}%");
context
.sql
.query_map_vec(
.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -207,6 +214,7 @@ impl Chatlist {
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
process_row,
process_rows,
)
.await?
} else {
@@ -221,7 +229,7 @@ impl Chatlist {
let msg_id: Option<MsgId> = row.get(3)?;
Ok((chat_id, typ, param, msg_id))
};
let process_rows = |rows: rusqlite::AndThenRows<_>| {
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
Ok((chat_id, typ, param, msg_id)) => {
if typ == Chattype::Mailinglist
@@ -235,6 +243,7 @@ impl Chatlist {
Err(e) => Some(Err(e)),
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
@@ -263,7 +272,7 @@ impl Chatlist {
).await?
} else {
// show normal chatlist
context.sql.query_map_vec(
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -281,6 +290,7 @@ impl Chatlist {
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
};
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
@@ -471,8 +481,8 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg,
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
remove_contact_from_chat, send_text_msg,
};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -485,9 +495,15 @@ mod tests {
async fn test_try_load() {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group(bob, "a chat").await.unwrap();
let chat_id2 = create_group(bob, "b chat").await.unwrap();
let chat_id3 = create_group(bob, "c chat").await.unwrap();
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
@@ -520,7 +536,9 @@ mod tests {
// receive a message from alice
let alice = &tcm.alice().await;
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
alice,
alice_chat_id,
@@ -558,7 +576,9 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
create_group(&t, "a chat").await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
@@ -745,7 +765,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group(&t, "a chat").await.unwrap();
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -761,7 +783,9 @@ mod tests {
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group(&t, "a chat").await.unwrap();
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -800,9 +824,15 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
let chat_id1 = create_group(&t, "a chat").await.unwrap();
create_group(&t, "b chat").await.unwrap();
create_group(&t, "c chat").await.unwrap();
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -14,15 +14,15 @@ use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, info};
use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{Provider, get_provider_by_id};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::ConfiguredLoginParam;
use crate::{constants, stats};
/// The available configuration keys.
#[derive(
@@ -156,6 +156,10 @@ pub enum Config {
#[strum(props(default = "1"))]
MdnsEnabled,
/// True if "Sent" folder should be watched for changes.
#[strum(props(default = "0"))]
SentboxWatch,
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
/// ones are moved there anyway.
#[strum(props(default = "1"))]
@@ -281,6 +285,9 @@ pub enum Config {
/// Configured folder for chat messages.
ConfiguredMvboxFolder,
/// Configured "Sent" folder.
ConfiguredSentboxFolder,
/// Configured "Trash" folder.
ConfiguredTrashFolder,
@@ -382,6 +389,12 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -401,22 +414,9 @@ pub enum Config {
/// used for signatures, encryption to self and included in `Autocrypt` header.
KeyId,
/// Send statistics to Delta Chat's developers.
/// Can be exposed to the user as a setting.
StatsSending,
/// Last time statistics were sent to Delta Chat's developers
StatsLastSent,
/// Last time `update_message_stats()` was called
StatsLastUpdate,
/// This key is sent to the statistics bot so that the bot can recognize the user
/// This key is sent to the self_reporting bot so that the bot can recognize the user
/// without storing the email address
StatsId,
/// The last contact id that already existed when statistics-sending was enabled for the first time.
StatsLastOldContactId,
SelfReportingId,
/// MsgId of webxdc map integration.
WebxdcIntegration,
@@ -466,7 +466,10 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
}
}
@@ -579,9 +582,8 @@ impl Context {
/// Returns boolean configuration value for the given key.
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self
.get_config(key)
.get_config_parsed::<i32>(key)
.await?
.and_then(|s| s.parse::<i32>().ok())
.map(|x| x != 0)
.unwrap_or_default())
}
@@ -593,6 +595,15 @@ impl Context {
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SentboxWatch).await?
&& self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some())
}
/// Returns true if sync messages should be sent.
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SyncMsgs).await?
@@ -681,6 +692,7 @@ impl Context {
| Config::ProxyEnabled
| Config::BccSelf
| Config::MdnsEnabled
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::DeleteToTrash
@@ -710,15 +722,9 @@ impl Context {
true => self.scheduler.pause(self).await?,
_ => Default::default(),
};
if key == Config::StatsSending {
let old_value = self.get_config(key).await?;
let old_value = bool_from_config(old_value.as_deref());
let new_value = bool_from_config(value);
stats::pre_sending_config_change(self, old_value, new_value).await?;
}
self.set_config_internal(key, value).await?;
if key == Config::StatsSending {
stats::maybe_send_stats(self).await?;
if key == Config::SentboxWatch {
self.last_full_folder_scan.lock().await.take();
}
Ok(())
}
@@ -871,10 +877,6 @@ pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
Some(if val { "1" } else { "0" })
}
pub(crate) fn bool_from_config(config: Option<&str>) -> bool {
config.is_some_and(|v| v.parse::<i32>().unwrap_or_default() != 0)
}
// Separate impl block for self address handling
impl Context {
/// Determine whether the specified addr maps to the/a self addr.

View File

@@ -28,20 +28,17 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::{LogExt, info, warn};
use crate::login_param::EnteredCertificateChecks;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
};
use crate::message::Message;
use crate::net::proxy::ProxyConfig;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
use deltachat_contact_tools::addr_cmp;
@@ -120,7 +117,7 @@ impl Context {
Ok(())
}
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -165,15 +162,20 @@ impl Context {
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
self.stop_io().await;
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
// and then calls configure(), which loads them again.
// At some point, we will remove configure()
// and then simplify the code
// to directly create an EnteredLoginParam.
let result = async move {
let mut param = match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
crate::qr::Qr::Login { address, options } => {
login_param_from_login_qr(&address, options)?
crate::qr::configure_from_login_qr(self, &address, options).await?
}
_ => bail!("QR code does not contain account"),
};
self.add_transport_inner(&mut param).await?;
}
self.configure().await?;
Ok(())
}
.await;
@@ -194,11 +196,16 @@ impl Context {
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let transports = self
.sql
.query_map_vec("SELECT entered_param FROM transports", (), |row| {
let entered_param: String = row.get(0)?;
let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
Ok(transport)
})
.query_map(
"SELECT entered_param FROM transports",
(),
|row| row.get::<_, String>(0),
|rows| {
rows.flatten()
.map(|s| Ok(serde_json::from_str(&s)?))
.collect::<Result<Vec<EnteredLoginParam>>>()
},
)
.await?;
Ok(transports)
@@ -293,6 +300,8 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let mut addr = param.addr.clone();
if param.oauth2 {
// the used oauth2 addr may differ, check this.
@@ -334,7 +343,7 @@ async fn get_configured_param(
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(&param_domain);
provider = provider::get_provider_info(ctx, &param_domain, proxy_enabled).await;
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
@@ -546,6 +555,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
true => ctx.get_config_bool(Config::IsChatmail).await?,
};
if is_chatmail {
ctx.set_config(Config::SentboxWatch, None).await?;
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
ctx.set_config(Config::ShowEmails, None).await?;
@@ -565,6 +575,14 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 910);
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
if configured_addr != param.addr {
// Switched account, all server UIDs we know are invalid
info!(ctx, "Scheduling resync because the address has changed.");
ctx.schedule_resync().await?;
}
}
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)

View File

@@ -28,9 +28,8 @@ struct MozAutoconfigure {
pub outgoing_servers: Vec<Server>,
}
#[derive(Debug, Default)]
#[derive(Debug)]
enum MozConfigTag {
#[default]
Undefined,
Hostname,
Port,
@@ -38,6 +37,12 @@ enum MozConfigTag {
Username,
}
impl Default for MozConfigTag {
fn default() -> Self {
Self::Undefined
}
}
impl FromStr for MozConfigTag {
type Err = ();

View File

@@ -101,8 +101,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
FromPrimitive,
ToPrimitive,
FromSql,
@@ -120,7 +118,7 @@ pub enum Chattype {
/// Group chat.
///
/// Created by [`crate::chat::create_group`].
/// Created by [`crate::chat::create_group_chat`].
Group = 120,
/// An (unencrypted) mailing list,
@@ -250,18 +248,6 @@ pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
/// Period between `sql::housekeeping()` runs.
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
pub(crate) const BROADCAST_INCOMPATIBILITY_MSG: &str = r#"The up to now "experimental channels feature" is about to become an officially supported one. By that, privacy will be improved, it will become faster, and less traffic will be consumed.
As we do not guarantee feature-stability for such experiments, this means, that you will need to create the channel again.
Here is what to do:
• Create a new channel
• Tap on the channel name
• Tap on "QR Invite Code"
• Have all recipients scan the QR code, or send them the link
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;

View File

@@ -36,7 +36,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time, to_lowercase};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -1282,13 +1282,14 @@ impl Contact {
let list = context
.sql
.query_map_vec(
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL,),
|row| {
let contact_id: ContactId = row.get(0)?;
Ok(contact_id)
}
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
Ok(list)
@@ -1573,10 +1574,19 @@ impl Contact {
Ok(None)
}
/// Returns a color for the contact.
/// See [`self::get_color`].
/// Get a color for the contact.
/// The color is calculated from the contact's fingerprint (for key-contacts)
/// or email address (for address-contacts) and can be used
/// for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
pub fn get_color(&self) -> u32 {
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
} else if self.id == ContactId::SELF {
0x808080
} else {
str_to_color(&self.addr.to_lowercase())
}
}
/// Gets the contact's status.
@@ -1672,21 +1682,6 @@ impl Contact {
}
}
/// Returns a color for a contact having given attributes.
///
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
/// avatar with white initials as well as for headlines in bubbles of group chats.
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
if let Some(fingerprint) = fingerprint {
str_to_color(&fingerprint.hex())
} else if is_self {
0x808080
} else {
str_to_color(&to_lowercase(addr))
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.
@@ -1789,7 +1784,9 @@ WHERE type=? AND id IN (
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
chat_id.unblock_ex(context, Nosync).await?;
}
}
@@ -2047,7 +2044,7 @@ impl RecentlySeenLoop {
// become unseen in the future.
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
.sql
.query_map_collect(
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
(now_ts - SEEN_RECENTLY_SECONDS,),
@@ -2056,6 +2053,10 @@ impl RecentlySeenLoop {
let last_seen: i64 = row.get("last_seen")?;
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
},
|rows| {
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
.map_err(Into::into)
},
)
.await
.unwrap_or_default();

View File

@@ -1,7 +1,7 @@
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
use super::*;
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
@@ -1320,6 +1320,9 @@ async fn test_self_is_verified() -> Result<()> {
assert!(contact.get_verifier_id(&alice).await?.is_none());
assert!(contact.is_key_contact());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}

View File

@@ -4,28 +4,33 @@ use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{ChatId, get_chat_cnt};
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
use crate::contact::{Contact, ContactId};
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
};
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::self_fingerprint;
use crate::key::{load_self_secret_key, self_fingerprint};
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::login_param::EnteredLoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::net::tls::TlsSessionStore;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
@@ -33,9 +38,7 @@ use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
use crate::tools::{self, duration_to_str, time, time_elapsed};
use crate::transport::ConfiguredLoginParam;
use crate::{chatlist_events, stats};
use crate::tools::{self, create_id, duration_to_str, time, time_elapsed};
/// Builder for the [`Context`].
///
@@ -243,6 +246,9 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
/// Notify about new messages.
///
/// This causes [`Context::wait_next_msgs`] to wake up.
@@ -256,6 +262,8 @@ pub struct InnerContext {
/// IMAP METADATA.
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
/// ID for this `Context` in the current process.
///
/// This allows for multiple `Context`s open in a single process where each context can
@@ -289,9 +297,6 @@ pub struct InnerContext {
/// True if account has subscribed to push notifications via IMAP.
pub(crate) push_subscribed: AtomicBool,
/// TLS session resumption cache.
pub(crate) tls_session_store: TlsSessionStore,
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
@@ -306,7 +311,7 @@ pub struct InnerContext {
}
/// The state of ongoing process.
#[derive(Debug, Default)]
#[derive(Debug)]
enum RunningState {
/// Ongoing process is allocated.
Running { cancel_sender: Sender<()> },
@@ -315,10 +320,15 @@ enum RunningState {
ShallStop { request: tools::Time },
/// There is no ongoing process, a new one can be allocated.
#[default]
Stopped,
}
impl Default for RunningState {
fn default() -> Self {
Self::Stopped
}
}
/// Return some info about deltachat-core
///
/// This contains information mostly about the library itself, the
@@ -454,16 +464,17 @@ impl Context {
scheduler: SchedulerState::new(),
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),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
metadata: RwLock::new(None),
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: parking_lot::RwLock::new("".to_string()),
migration_error: parking_lot::RwLock::new(None),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
tls_session_store: TlsSessionStore::new(),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
connectivities: parking_lot::Mutex::new(Vec::new()),
@@ -612,6 +623,12 @@ impl Context {
Ok(())
}
pub(crate) async fn schedule_resync(&self) -> Result<()> {
self.resync_request.store(true, Ordering::Relaxed);
self.scheduler.interrupt_inbox().await;
Ok(())
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
@@ -804,6 +821,7 @@ impl Context {
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
let request_msgs = message::get_request_msg_cnt(self).await;
let contacts = Contact::get_real_cnt(self).await?;
let is_configured = self.get_config_int(Config::Configured).await?;
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
let dbversion = self
.sql
@@ -831,6 +849,7 @@ impl Context {
Err(err) => format!("<key failure: {err}>"),
};
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
let folders_configured = self
@@ -843,6 +862,10 @@ impl Context {
.get_config(Config::ConfiguredInboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await?
@@ -877,6 +900,7 @@ impl Context {
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("proxy_enabled", proxy_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2);
@@ -930,6 +954,7 @@ impl Context {
.await?
.to_string(),
);
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert(
@@ -937,6 +962,7 @@ impl Context {
folders_configured.to_string(),
);
res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
@@ -1004,6 +1030,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
@@ -1035,22 +1067,6 @@ impl Context {
.await?
.unwrap_or_default(),
);
res.insert(
"stats_id",
self.get_config(Config::StatsId)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"stats_sending",
stats::should_send_stats(self).await?.to_string(),
);
res.insert(
"stats_last_sent",
self.get_config_i64(Config::StatsLastSent)
.await?
.to_string(),
);
res.insert(
"fail_on_receiving_full_msg",
self.sql
@@ -1065,6 +1081,147 @@ impl Context {
Ok(res)
}
async fn get_self_report(&self) -> Result<String> {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
unencrypted_mua: u32,
}
let mut res = String::new();
res += &format!("core_version {}\n", get_version_str());
let num_msgs: u32 = self
.sql
.query_get_value(
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?",
(DC_CHAT_ID_TRASH,),
)
.await?
.unwrap_or_default();
res += &format!("num_msgs {num_msgs}\n");
let num_chats: u32 = self
.sql
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
.await?
.unwrap_or_default();
res += &format!("num_chats {num_chats}\n");
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
res += &format!("db_size_bytes {db_size}\n");
let secret_key = &load_self_secret_key(self).await?.primary_key;
let key_created = secret_key.public_key().created_at().timestamp();
res += &format!("key_created {key_created}\n");
// how many of the chats active in the last months are:
// - protected
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
// - unencrypted and the contact uses a classical MUA
let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
let chats = self
.sql
.query_map(
"SELECT c.protected, m.param, m.msgrmsg
FROM chats c
JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND hidden=0
AND download_state=?
AND to_id!=?
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND (c.blocked=0 OR c.blocked=2)
AND IFNULL(m.timestamp,c.created_timestamp) > ?
GROUP BY c.id",
(DownloadState::Done, ContactId::INFO, three_months_ago),
|row| {
let protected: ProtectionStatus = row.get(0)?;
let message_param: Params =
row.get::<_, String>(1)?.parse().unwrap_or_default();
let is_dc_message: bool = row.get(2)?;
Ok((protected, message_param, is_dc_message))
},
|rows| {
let mut chats = ChatNumbers::default();
for row in rows {
let (protected, message_param, is_dc_message) = row?;
let encrypted = message_param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or(false);
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
} else {
chats.opportunistic_mua += 1;
}
} else if is_dc_message {
chats.unencrypted_dc += 1;
} else {
chats.unencrypted_mua += 1;
}
}
Ok(chats)
},
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
Some(id) => id,
None => {
let id = create_id();
self.set_config(Config::SelfReportingId, Some(&id)).await?;
id
}
};
res += &format!("self_reporting_id {self_reporting_id}");
Ok(res)
}
/// Drafts a message with statistics about the usage of Delta Chat.
/// The user can inspect the message if they want, and then hit "Send".
///
/// On the other end, a bot will receive the message and make it available
/// to Delta Chat's developers.
pub async fn draft_self_report(&self) -> Result<ChatId> {
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
.await?
.first()
.context("Self reporting bot vCard does not contain a contact")?;
mark_contact_id_as_verified(self, contact_id, Some(ContactId::SELF)).await?;
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
let mut msg = Message::new_text(self.get_self_report().await?);
chat_id.set_draft(self, Some(&mut msg)).await?;
Ok(chat_id)
}
/// Get a list of fresh, unmuted messages in unblocked chats.
///
/// The list starts with the most recent message
@@ -1074,7 +1231,7 @@ impl Context {
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
let list = self
.sql
.query_map_vec(
.query_map(
concat!(
"SELECT m.id",
" FROM msgs m",
@@ -1091,9 +1248,13 @@ impl Context {
" ORDER BY m.timestamp DESC,m.id DESC;"
),
(MessageState::InFresh, time()),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
|row| row.get::<_, MsgId>(0),
|rows| {
let mut list = Vec::new();
for row in rows {
list.push(row?);
}
Ok(list)
},
)
.await?;
@@ -1127,7 +1288,7 @@ impl Context {
let list = self
.sql
.query_map_vec(
.query_map(
"SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
@@ -1147,6 +1308,13 @@ impl Context {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
|rows| {
let mut list = Vec::new();
for row in rows {
list.push(row?);
}
Ok(list)
},
)
.await?;
Ok(list)
@@ -1187,7 +1355,7 @@ impl Context {
let list = if let Some(chat_id) = chat_id {
self.sql
.query_map_vec(
.query_map(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
@@ -1198,9 +1366,13 @@ impl Context {
AND IFNULL(txt_normalized, txt) LIKE ?
ORDER BY m.timestamp,m.id;",
(chat_id, str_like_in_text),
|row| {
let msg_id: MsgId = row.get("id")?;
Ok(msg_id)
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
@@ -1216,7 +1388,7 @@ impl Context {
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
self.sql
.query_map_vec(
.query_map(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
@@ -1230,9 +1402,13 @@ impl Context {
AND IFNULL(txt_normalized, txt) LIKE ?
ORDER BY m.id DESC LIMIT 1000",
(str_like_in_text,),
|row| {
let msg_id: MsgId = row.get("id")?;
Ok(msg_id)
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
@@ -1247,6 +1423,12 @@ impl Context {
Ok(inbox.as_deref() == Some(folder_name))
}
/// Returns true if given folder name is the name of the "sent" folder.
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox.as_deref() == Some(folder_name))
}
/// Returns true if given folder name is the name of the "DeltaChat" folder.
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;

View File

@@ -6,9 +6,9 @@ use super::*;
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -276,6 +276,7 @@ async fn test_get_info_completeness() {
"mail_port",
"mail_security",
"notify_about_wrong_pw",
"self_reporting_id",
"selfstatus",
"send_server",
"send_user",
@@ -295,8 +296,6 @@ async fn test_get_info_completeness() {
"webxdc_integration",
"device_token",
"encrypted_device_token",
"stats_last_update",
"stats_last_old_contact_id",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
@@ -599,6 +598,26 @@ async fn test_get_next_msgs() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_draft_self_report() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = alice.draft_self_report().await?;
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_protected());
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
assert!(draft.text.starts_with("core_version"));
// Test that sending into the protected chat works:
let _sent = alice.send_msg(chat_id, &mut draft).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -10,19 +10,17 @@ use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message was encrypted,
/// returns the decrypted and decompressed message.
/// If successful and the message is encrypted, returns decrypted body.
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
shared_secrets: &[String],
) -> Result<Option<::pgp::composed::Message<'static>>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::decrypt(data, private_keyring, shared_secrets)?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
Ok(Some(msg))
}

View File

@@ -157,23 +157,30 @@ pub(crate) async fn download_msg(
let row = context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
"SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
let uidvalidity: u32 = row.get(2)?;
Ok((server_uid, server_folder, uidvalidity))
},
)
.await?;
let Some((server_uid, server_folder)) = row else {
let Some((server_uid, server_folder, uidvalidity)) = row else {
// No IMAP record found, we don't know the UID and folder.
return Err(anyhow!("Call download_full() again to try over."));
};
session
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.fetch_single_msg(
context,
&server_folder,
uidvalidity,
server_uid,
msg.rfc724_mid.clone(),
)
.await?;
Ok(())
}
@@ -187,6 +194,7 @@ impl Session {
&mut self,
context: &Context,
folder: &str,
uidvalidity: u32,
uid: u32,
rfc724_mid: String,
) -> Result<()> {
@@ -206,8 +214,16 @@ impl Session {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (sender, receiver) = async_channel::unbounded();
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender)
.await?;
self.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
sender,
)
.await?;
if receiver.recv().await.is_err() {
bail!("Failed to fetch UID {uid}");
}
@@ -221,14 +237,21 @@ impl MimeMessage {
/// To create the placeholder, only the outermost header can be used,
/// the mime-structure itself is not available.
///
/// The placeholder part currently contains a text with size and availability of the message.
/// The placeholder part currently contains a text with size and availability of the message;
/// `error` is set as the part error;
/// in the future, we may do more advanced things as previews here.
pub(crate) async fn create_stub_from_partial_download(
&mut self,
context: &Context,
org_bytes: u32,
error: Option<String>,
) -> Result<()> {
let prefix = match error {
None => "",
Some(_) => "[❗] ",
};
let mut text = format!(
"[{}]",
"{prefix}[{}]",
stock_str::partial_download_msg_body(context, org_bytes).await
);
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
@@ -245,6 +268,7 @@ impl MimeMessage {
self.do_add_single_part(Part {
typ: Viewtype::Text,
msg: text,
error,
..Default::default()
});

View File

@@ -46,7 +46,6 @@ impl EncryptHelper {
keyring: Vec<SignedPublicKey>,
mail_to_encrypt: MimePart<'static>,
compress: bool,
anonymous_recipients: bool,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
@@ -54,35 +53,7 @@ impl EncryptHelper {
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = pgp::pk_encrypt(
raw_message,
keyring,
Some(sign_key),
compress,
anonymous_recipients,
)
.await?;
Ok(ctext)
}
/// Symmetrically encrypt the message. This is used for broadcast channels.
/// `shared secret` is the secret that will be used for symmetric encryption.
pub async fn encrypt_symmetrically(
self,
context: &Context,
shared_secret: &str,
mail_to_encrypt: MimePart<'static>,
compress: bool,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut raw_message = Vec::new();
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext =
pgp::symm_encrypt_message(raw_message, sign_key, shared_secret, compress).await?;
let ctext = pgp::pk_encrypt(raw_message, keyring, Some(sign_key), compress).await?;
Ok(ctext)
}

View File

@@ -80,18 +80,17 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::location;
use crate::log::{LogExt, error, info, warn};
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::tools::{SystemTime, duration_to_str, time};
use crate::{location, stats};
/// Ephemeral timer value.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {
/// Timer is disabled.
#[default]
Disabled,
/// Timer is enabled.
@@ -126,6 +125,12 @@ impl Timer {
}
}
impl Default for Timer {
fn default() -> Self {
Self::Disabled
}
}
impl fmt::Display for Timer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_u32())
@@ -381,7 +386,7 @@ async fn select_expired_messages(
) -> Result<Vec<(MsgId, ChatId, Viewtype, u32)>> {
let mut rows = context
.sql
.query_map_vec(
.query_map(
r#"
SELECT id, chat_id, type, location_id
FROM msgs
@@ -402,6 +407,7 @@ WHERE
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
@@ -419,7 +425,7 @@ WHERE
let rows_expired = context
.sql
.query_map_vec(
.query_map(
r#"
SELECT id, chat_id, type, location_id
FROM msgs
@@ -447,6 +453,7 @@ WHERE
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
@@ -603,7 +610,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
+ Duration::from_secs(1)
} else {
// no messages to be deleted for now, wait long for one to occur
now + Duration::from_secs(86400) // 1 day
now + Duration::from_secs(86400)
};
if let Ok(duration) = until.duration_since(now) {
@@ -630,12 +637,6 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
}
}
// Make sure that the statistics stay correct by updating them _before_ deleting messages:
stats::maybe_update_message_stats(context)
.await
.log_err(context)
.ok();
delete_expired_messages(context, time())
.await
.log_err(context)

View File

@@ -12,7 +12,7 @@ use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::{
chat::{self, Chat, ChatItem, create_group, send_text_msg},
chat::{self, Chat, ChatItem, ProtectionStatus, create_group_chat, send_text_msg},
tools::IsNoneOrEmpty,
};
@@ -164,7 +164,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
async fn test_ephemeral_unpromoted() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group(&alice, "Group name").await?;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
// Group is unpromoted, the timer can be changed without sending a message.
assert!(chat_id.is_unpromoted(&alice).await?);
@@ -799,7 +799,8 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = create_group(alice, "Group name").await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?;
@@ -825,68 +826,3 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
Ok(())
}
/// Tests that expiration of a disappearing message
/// with unknown viewtype does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_disappearing_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
let duration = 60;
chat.id
.set_ephemeral_timer(alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new_text("Expiring message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(100));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}
/// Tests that deletion of a message with unknown viewtype
/// triggered by `delete_device_after`
/// does not make `delete_expired_messages` fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_device_after_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
alice
.set_config(Config::DeleteDeviceAfter, Some("600"))
.await?;
let mut msg = Message::new_text("Some message".to_string());
let _alice_sent_message = alice.send_msg(chat.id, &mut msg).await;
// Set message viewtype to unassigned
// type 70 that was previously used for videochat invitations.
alice
.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg.id,))
.await?;
SystemTime::shift(Duration::from_secs(1000));
// This should not fail.
delete_expired_messages(alice, time()).await?;
Ok(())
}

View File

@@ -66,7 +66,8 @@ mod test_chatlist_events {
use crate::{
EventType,
chat::{
self, ChatId, ChatVisibility, MuteDuration, create_broadcast, create_group, set_muted,
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
create_group_chat, set_muted,
},
config::Config,
constants::*,
@@ -137,7 +138,12 @@ mod test_chatlist_events {
async fn test_change_chat_visibility() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat_id = create_group(&alice, "my_group").await?;
let chat_id = create_group_chat(
&alice,
crate::chat::ProtectionStatus::Unprotected,
"my_group",
)
.await?;
chat_id
.set_visibility(&alice, ChatVisibility::Pinned)
@@ -283,7 +289,7 @@ mod test_chatlist_events {
async fn test_delete_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat.delete(&alice).await?;
@@ -293,11 +299,11 @@ mod test_chatlist_events {
/// Create group chat
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group() -> Result<()> {
async fn test_create_group_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.evtracker.clear_events();
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
wait_for_chatlist_and_specific_item(&alice, chat).await;
Ok(())
}
@@ -318,7 +324,7 @@ mod test_chatlist_events {
async fn test_mute_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
@@ -337,7 +343,7 @@ mod test_chatlist_events {
async fn test_mute_chat_expired() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let mute_duration = MuteDuration::Until(
std::time::SystemTime::now()
@@ -357,7 +363,7 @@ mod test_chatlist_events {
async fn test_change_chat_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat::set_chat_name(&alice, chat, "New Name").await?;
@@ -371,7 +377,7 @@ mod test_chatlist_events {
async fn test_change_chat_profile_image() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
let file = alice.dir.path().join("avatar.png");
@@ -389,7 +395,9 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -411,7 +419,9 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -428,7 +438,9 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -444,7 +456,7 @@ mod test_chatlist_events {
async fn test_delete_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?;
alice.evtracker.clear_events();
@@ -461,7 +473,9 @@ mod test_chatlist_events {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
chat_id_for_bob.accept(&bob).await?;
@@ -502,7 +516,7 @@ mod test_chatlist_events {
async fn test_update_after_ephemeral_messages() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 60 })
.await?;
alice
@@ -546,7 +560,8 @@ First thread."#;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chatid = chat::create_group(&alice.ctx, "the chat").await?;
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?;
@@ -593,7 +608,7 @@ First thread."#;
async fn test_resend_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;
@@ -613,7 +628,7 @@ First thread."#;
async fn test_reaction() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group(&alice, "My Group").await?;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;

View File

@@ -39,8 +39,6 @@ pub enum HeaderDef {
/// Mailing list ID defined in [RFC 2919](https://tools.ietf.org/html/rfc2919).
ListId,
ListPost,
/// Mailing list id, belonging to a broadcast channel created by Delta Chat
ChatListId,
/// List-Help header defined in [RFC 2369](https://datatracker.ietf.org/doc/html/rfc2369).
ListHelp,
@@ -65,9 +63,7 @@ pub enum HeaderDef {
ChatUserAvatar,
ChatVoiceMessage,
ChatGroupMemberRemoved,
ChatGroupMemberRemovedFpr,
ChatGroupMemberAdded,
ChatGroupMemberAddedFpr,
ChatContent,
/// Past members of the group.
@@ -98,11 +94,6 @@ pub enum HeaderDef {
/// This message obsoletes the text of the message defined here by rfc724_mid.
ChatEdit,
/// The secret shared amongst all recipients of this broadcast channel,
/// used to encrypt and decrypt messages.
/// This secret is sent to a new member in the member-addition message.
ChatBroadcastSecret,
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,

View File

@@ -20,11 +20,12 @@ use deltachat_contact_tools::ContactAddress;
use futures::{FutureExt as _, TryStreamExt};
use futures_lite::FutureExt;
use num_traits::FromPrimitive;
use rand::Rng;
use ratelimit::Ratelimit;
use url::Url;
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, ShowEmails};
@@ -33,6 +34,9 @@ use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::{LogExt, error, info, warn};
use crate::login_param::{
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
@@ -45,9 +49,6 @@ use crate::receive_imf::{
use crate::scheduler::connectivity::ConnectivityStore;
use crate::stock_str;
use crate::tools::{self, create_id, duration_to_str, time};
use crate::transport::{
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
};
pub(crate) mod capabilities;
mod client;
@@ -104,12 +105,6 @@ pub(crate) struct Imap {
/// immediately after logging in or returning an error in response to LOGIN command
/// due to internal server error.
ratelimit: Ratelimit,
/// IMAP UID resync request sender.
pub(crate) resync_request_sender: async_channel::Sender<()>,
/// IMAP UID resync request receiver.
pub(crate) resync_request_receiver: async_channel::Receiver<()>,
}
#[derive(Debug)]
@@ -162,6 +157,7 @@ pub enum FolderMeaning {
Spam,
Inbox,
Mvbox,
Sent,
Trash,
/// Virtual folders.
@@ -180,6 +176,7 @@ impl FolderMeaning {
FolderMeaning::Spam => None,
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
FolderMeaning::Virtual => None,
}
@@ -260,7 +257,6 @@ impl Imap {
oauth2: bool,
idle_interrupt_receiver: Receiver<()>,
) -> Self {
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
Imap {
idle_interrupt_receiver,
addr: addr.to_string(),
@@ -275,8 +271,6 @@ impl Imap {
conn_backoff_ms: 0,
// 1 connection per minute + a burst of 2.
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
resync_request_sender,
resync_request_receiver,
}
}
@@ -348,9 +342,9 @@ impl Imap {
const BACKOFF_MIN_MS: u64 = 2000;
const BACKOFF_MAX_MS: u64 = 80_000;
self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
(self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
));
self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(
rand::thread_rng().gen_range((self.conn_backoff_ms / 2)..=self.conn_backoff_ms),
);
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
@@ -401,7 +395,6 @@ impl Imap {
match login_res {
Ok(mut session) => {
let capabilities = determine_capabilities(&mut session).await?;
let resync_request_sender = self.resync_request_sender.clone();
let session = if capabilities.can_compress {
info!(context, "Enabling IMAP compression.");
@@ -412,9 +405,9 @@ impl Imap {
})
.await
.context("Failed to enable IMAP compression")?;
Session::new(compressed_session, capabilities, resync_request_sender)
Session::new(compressed_session, capabilities)
} else {
Session::new(session, capabilities, resync_request_sender)
Session::new(session, capabilities)
};
// Store server ID in the context to display in account info.
@@ -627,38 +620,71 @@ impl Imap {
// Determine the target folder where the message should be moved to.
//
// We only move the messages from the INBOX and Spam folders.
// If we have seen the message on the IMAP server before, do not move it.
// This is required to avoid infinite MOVE loop on IMAP servers
// that alias `DeltaChat` folder to other names.
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
// results in the messages getting a new UID,
// so the messages will be detected as new
// In this case Delta Chat configured with `DeltaChat` as the destination folder
// would detect messages in the `INBOX.DeltaChat` folder
// and try to move them to the `DeltaChat` folder.
// Such move to the same folder results in the messages
// getting a new UID, so the messages will be detected as new
// in the `INBOX.DeltaChat` folder again.
let delete = if let Some(message_id) = &message_id {
message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
let _target;
let target = if let Some(message_id) = &message_id {
let msg_info =
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
let delete = if let Some((_, _, true)) = msg_info {
info!(context, "Deleting locally deleted message {message_id}.");
true
} else if let Some((_, ts_sent_old, _)) = msg_info {
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let ts_sent = headers
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
if is_dup {
info!(context, "Deleting duplicate message {message_id}.");
}
is_dup
} else {
false
};
if delete {
&delete_target
} else if context
.sql
.exists(
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
(message_id,),
)
.await?
.is_some_and(|(_msg_id, deleted)| deleted)
{
info!(
context,
"Not moving the message {} that we have seen before.", &message_id
);
folder
} else {
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
}
} else {
false
// Do not move the messages without Message-ID.
// We cannot reliably determine if we have seen them before,
// so it is safer not to move them.
warn!(
context,
"Not moving the message that does not have a Message-ID."
);
folder
};
// Generate a fake Message-ID to identify the message in the database
// if the message has no real Message-ID.
let message_id = message_id.unwrap_or_else(create_message_id);
if delete {
info!(context, "Deleting locally deleted message {message_id}.");
}
let _target;
let target = if delete {
&delete_target
} else {
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
};
context
.sql
.execute(
@@ -742,6 +768,7 @@ impl Imap {
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
@@ -806,6 +833,9 @@ impl Imap {
context: &Context,
session: &mut Session,
) -> Result<()> {
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
.await
.context("failed to get recipients from the sentbox")?;
add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
.await
.context("failed to get recipients from the movebox")?;
@@ -1040,7 +1070,7 @@ impl Session {
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
.sql
.query_map_vec(
.query_map(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
@@ -1052,6 +1082,7 @@ impl Session {
let target: String = row.get(2)?;
Ok((rowid, uid, target))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
@@ -1142,7 +1173,7 @@ impl Session {
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
let rows = context
.sql
.query_map_vec(
.query_map(
"SELECT imap.id, uid, folder FROM imap, imap_markseen
WHERE imap.id = imap_markseen.id AND target = folder
ORDER BY folder, uid",
@@ -1153,6 +1184,7 @@ impl Session {
let folder: String = row.get(2)?;
Ok((rowid, uid, folder))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
@@ -1351,10 +1383,12 @@ impl Session {
///
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
folder: &str,
uidvalidity: u32,
request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
@@ -1478,19 +1512,35 @@ impl Session {
context,
"Passing message UID {} to receive_imf().", request_uid
);
let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await;
let received_msg = match res {
Err(err) => {
warn!(context, "receive_imf error: {err:#}.");
let text = format!(
"❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
);
let mut msg = Message::new_text(text);
add_device_msg(context, None, Some(&mut msg)).await?;
None
let res = receive_imf_inner(
context,
folder,
uidvalidity,
request_uid,
rfc724_mid,
body,
is_seen,
partial.map(|msg_size| (msg_size, None)),
)
.await;
let received_msg = if let Err(err) = res {
warn!(context, "receive_imf error: {:#}.", err);
if partial.is_some() {
return Err(err);
}
Ok(msg) => msg,
receive_imf_inner(
context,
folder,
uidvalidity,
request_uid,
rfc724_mid,
body,
is_seen,
Some((body.len().try_into()?, Some(format!("{err:#}")))),
)
.await?
} else {
res?
};
received_msgs_channel
.send((request_uid, received_msg))
@@ -2039,7 +2089,7 @@ async fn spam_target_folder_cfg(
if needs_move_to_mvbox(context, headers).await?
// If OnlyFetchMvbox is set, we don't want to move the message to
// the inbox where we wouldn't fetch it again:
// the inbox or sentbox where we wouldn't fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
@@ -2048,7 +2098,7 @@ async fn spam_target_folder_cfg(
}
}
/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
/// Returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder` if
/// the message needs to be moved from `folder`. Otherwise returns `None`.
pub async fn target_folder_cfg(
context: &Context,
@@ -2062,9 +2112,7 @@ pub async fn target_folder_cfg(
if folder_meaning == FolderMeaning::Spam {
spam_target_folder_cfg(context, headers).await
} else if folder_meaning == FolderMeaning::Inbox
&& needs_move_to_mvbox(context, headers).await?
{
} else if needs_move_to_mvbox(context, headers).await? {
Ok(Some(Config::ConfiguredMvboxFolder))
} else {
Ok(None)
@@ -2137,6 +2185,38 @@ async fn needs_move_to_mvbox(
// but sth. different in others - a hard job.
fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
// source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
const SENT_NAMES: &[&str] = &[
"sent",
"sentmail",
"sent objects",
"gesendet",
"Sent Mail",
"Sendte e-mails",
"Enviados",
"Messages envoyés",
"Messages envoyes",
"Posta inviata",
"Verzonden berichten",
"Wyslane",
"E-mails enviados",
"Correio enviado",
"Enviada",
"Enviado",
"Gönderildi",
"Inviati",
"Odeslaná pošta",
"Sendt",
"Skickat",
"Verzonden",
"Wysłane",
"Éléments envoyés",
"Απεσταλμένα",
"Отправленные",
"寄件備份",
"已发送邮件",
"送信済み",
"보낸편지함",
];
const SPAM_NAMES: &[&str] = &[
"spam",
"junk",
@@ -2180,8 +2260,8 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
];
let lower = folder_name.to_lowercase();
if lower == "inbox" {
FolderMeaning::Inbox
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Sent
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Spam
} else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
@@ -2195,6 +2275,7 @@ fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning
for attr in folder_attrs {
match attr {
NameAttribute::Trash => return FolderMeaning::Trash,
NameAttribute::Sent => return FolderMeaning::Sent,
NameAttribute::Junk => return FolderMeaning::Spam,
NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
NameAttribute::Extension(label) => {
@@ -2335,6 +2416,15 @@ pub(crate) async fn prefetch_should_download(
Ok(should_download)
}
/// Returns whether a message is a duplicate (resent message).
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
// because they are stored to the db before sending. Also consider as duplicates only messages
// with greater timestamp to avoid deleting both messages in a multi-device setting.
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
}
/// Marks messages in `msgs` table as seen, searching for them by UID.
///
/// Returns updated chat ID if any message was marked as seen.
@@ -2528,6 +2618,10 @@ async fn should_ignore_folder(
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
if context.is_sentbox(folder).await? {
// Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
}
Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
}

View File

@@ -9,14 +9,13 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use crate::context::Context;
use crate::log::{LoggingStream, info, warn};
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
use crate::tools::time;
use crate::transport::ConnectionCandidate;
use crate::transport::ConnectionSecurity;
#[derive(Debug)]
pub(crate) struct Client {
@@ -38,12 +37,12 @@ impl DerefMut for Client {
}
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static str {
fn alpn(port: u16) -> &'static [&'static str] {
if port == 993 {
// Do not request ALPN on standard port.
""
&[]
} else {
"imap"
&["imap"]
}
}
@@ -211,15 +210,7 @@ impl Client {
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
let tls_stream = wrap_tls(
strict_tls,
hostname,
addr.port(),
alpn(addr.port()),
logging_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -271,16 +262,9 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
"",
tcp_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
@@ -297,15 +281,7 @@ impl Client {
let proxy_stream = proxy_config
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(
strict_tls,
domain,
port,
alpn(port),
proxy_stream,
&context.tls_session_store,
)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -358,16 +334,9 @@ impl Client {
let buffered_proxy_stream = client.into_inner();
let proxy_stream = buffered_proxy_stream.into_inner();
let tls_stream = wrap_tls(
strict_tls,
hostname,
port,
"",
proxy_stream,
&context.tls_session_store,
)
.await
.context("STARTTLS upgrade failed")?;
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);

View File

@@ -3,6 +3,17 @@ use crate::test_utils::TestContext;
#[test]
fn test_get_folder_meaning_by_name() {
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
assert_eq!(
get_folder_meaning_by_name("Messages envoyés"),
FolderMeaning::Sent
);
assert_eq!(
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
FolderMeaning::Sent
);
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
@@ -108,6 +119,9 @@ async fn check_target_folder_combination(
t.ctx
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
.await?;
t.ctx
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
.await?;
t.ctx
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
.await?;
@@ -169,6 +183,10 @@ const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Sent", false, false, "Sent"),
("Sent", false, true, "Sent"),
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", false, true, "INBOX"),
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
@@ -181,6 +199,10 @@ const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Sent", false, false, "Sent"),
("Sent", false, true, "Sent"),
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "Spam"),
("Spam", false, true, "INBOX"),
("Spam", true, false, "Spam"),

View File

@@ -18,7 +18,7 @@ impl Imap {
) -> Result<bool> {
// First of all, debounce to once per minute:
{
let mut last_scan = session.last_full_folder_scan.lock().await;
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = time_elapsed(&last_scan).as_secs();
let debounce_secs = context
@@ -84,15 +84,21 @@ impl Imap {
}
}
// Set config for the Trash folder. Or reset if the folder was deleted.
let conf = Config::ConfiguredTrashFolder;
let val = folder_configs.get(&conf).map(|s| s.as_str());
let interrupt = val.is_some() && context.get_config(conf).await?.is_none();
context.set_config_internal(conf, val).await?;
if interrupt {
// `Imap::fetch_move_delete()`, particularly message deletion, is possible now for other
// folders (NB: we are in the Inbox loop).
context.scheduler.interrupt_oboxes().await;
// Set configs for necessary folders. Or reset if the folder was deleted.
for conf in [
Config::ConfiguredSentboxFolder,
Config::ConfiguredTrashFolder,
] {
let val = folder_configs.get(&conf).map(|s| s.as_str());
let interrupt = conf == Config::ConfiguredTrashFolder
&& val.is_some()
&& context.get_config(conf).await?.is_none();
context.set_config_internal(conf, val).await?;
if interrupt {
// `Imap::fetch_move_delete()` is possible now for other folders (NB: we are in the
// Inbox loop).
context.scheduler.interrupt_oboxes().await;
}
}
info!(context, "Found folders: {folder_names:?}.");
@@ -102,6 +108,9 @@ impl Imap {
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.get_config_bool(Config::SentboxWatch).await? {
res.push(Config::ConfiguredSentboxFolder);
}
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}

View File

@@ -206,7 +206,7 @@ impl ImapSession {
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, new_uid_next).await?;
self.resync_request_sender.try_send(()).ok();
context.schedule_resync().await?;
}
// If UIDNEXT changed, there are new emails.
@@ -243,7 +243,7 @@ impl ImapSession {
.await?;
if old_uid_validity != 0 || old_uid_next != 0 {
self.resync_request_sender.try_send(()).ok();
context.schedule_resync().await?;
}
info!(
context,

View File

@@ -5,11 +5,9 @@ use anyhow::{Context as _, Result};
use async_imap::Session as ImapSession;
use async_imap::types::Mailbox;
use futures::TryStreamExt;
use tokio::sync::Mutex;
use crate::imap::capabilities::Capabilities;
use crate::net::session::SessionStream;
use crate::tools;
/// Prefetch:
/// - Message-ID to check if we already have the message.
@@ -42,14 +40,10 @@ pub(crate) struct Session {
pub selected_folder_needs_expunge: bool,
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
/// True if currently selected folder has new messages.
///
/// Should be false if no folder is currently selected.
pub new_mail: bool,
pub resync_request_sender: async_channel::Sender<()>,
}
impl Deref for Session {
@@ -70,7 +64,6 @@ impl Session {
pub(crate) fn new(
inner: ImapSession<Box<dyn SessionStream>>,
capabilities: Capabilities,
resync_request_sender: async_channel::Sender<()>,
) -> Self {
Self {
inner,
@@ -78,9 +71,7 @@ impl Session {
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
last_full_folder_scan: Mutex::new(None),
new_mail: false,
resync_request_sender,
}
}

View File

@@ -648,7 +648,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
let keys = context
.sql
.query_map_vec(
.query_map(
"SELECT id, public_key, private_key, id=(SELECT value FROM config WHERE keyname='key_id') FROM keypairs;",
(),
|row| {
@@ -661,6 +661,10 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let self_addr = context.get_primary_self_addr().await?;

View File

@@ -1,6 +1,8 @@
//! # Key transfer via Autocrypt Setup Message.
use std::io::BufReader;
use rand::{Rng, thread_rng};
use anyhow::{Result, bail, ensure};
use crate::blob::BlobObject;
@@ -93,7 +95,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
let private_key = load_self_secret_key(context).await?;
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt_autocrypt_setup(passphrase, private_key_asc.into_bytes())
let encr = pgp::symm_encrypt(passphrase, private_key_asc.into_bytes())
.await?
.replace('\n', "\r\n");
@@ -131,11 +133,12 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
/// Creates a new setup code for Autocrypt Setup Message.
fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut rng = thread_rng();
let mut ret = String::new();
for i in 0..9 {
loop {
random_val = rand::random();
random_val = rng.r#gen();
if random_val as usize <= 60000 {
break;
}

View File

@@ -1,38 +0,0 @@
//! Re-exports of `pub(crate)` functions that are needed for benchmarks.
#![allow(missing_docs)] // Not necessary to put a doc comment on the pub functions here
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use crate::chat::ChatId;
use crate::context::Context;
use crate::key;
use crate::key::DcKey;
use crate::mimeparser::MimeMessage;
use crate::pgp;
use crate::pgp::KeyPair;
pub fn key_from_asc(data: &str) -> Result<key::SignedSecretKey> {
key::SignedSecretKey::from_asc(data)
}
pub async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<()> {
key::store_self_keypair(context, keypair).await
}
pub async fn parse_and_get_text(context: &Context, imf_raw: &[u8]) -> Result<String> {
let mime_parser = MimeMessage::from_bytes(context, imf_raw, None).await?;
Ok(mime_parser.parts.into_iter().next().unwrap().msg)
}
pub async fn save_broadcast_secret(context: &Context, chat_id: ChatId, secret: &str) -> Result<()> {
crate::chat::save_broadcast_secret(context, chat_id, secret).await
}
pub fn create_dummy_keypair(addr: &str) -> Result<KeyPair> {
pgp::create_keypair(EmailAddress::new(addr)?)
}
pub fn create_broadcast_secret() -> String {
crate::tools::create_broadcast_secret()
}

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