mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
120 Commits
v1.136.6
...
hoc/try-ou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2580ec2201 | ||
|
|
d2014f6038 | ||
|
|
ba6f3291ea | ||
|
|
5d34b225b7 | ||
|
|
6ca6a439bd | ||
|
|
f9465f7512 | ||
|
|
489eae5d66 | ||
|
|
b6c6a63a39 | ||
|
|
c069190b68 | ||
|
|
94ac2b1097 | ||
|
|
6080a52024 | ||
|
|
0aea7d1e02 | ||
|
|
08cbc54c00 | ||
|
|
9731ec419e | ||
|
|
e9cfcd9d1b | ||
|
|
d39cbcdc8d | ||
|
|
fbbefe6b49 | ||
|
|
bab311730c | ||
|
|
b47cad7e68 | ||
|
|
a3b62b9743 | ||
|
|
9aa4c0e56b | ||
|
|
27d2b12e8d | ||
|
|
c1148e4117 | ||
|
|
295f7a291b | ||
|
|
2be28f1311 | ||
|
|
2e42243de8 | ||
|
|
00f2585d8c | ||
|
|
0b73f9cebd | ||
|
|
f5e8a04fd0 | ||
|
|
6721df7d57 | ||
|
|
18d98d643b | ||
|
|
62758658ed | ||
|
|
03bb751a9b | ||
|
|
3ebb1ea95f | ||
|
|
c1d251010f | ||
|
|
7e5959e495 | ||
|
|
823da56f2d | ||
|
|
5bcc44ca9b | ||
|
|
4304e3f0be | ||
|
|
e2e3abdf03 | ||
|
|
dcea188b62 | ||
|
|
5cf725a378 | ||
|
|
2bf0ea9d91 | ||
|
|
1df936aeac | ||
|
|
9ab2c6df16 | ||
|
|
cf11741a8c | ||
|
|
b6a12e3914 | ||
|
|
b753440a68 | ||
|
|
39abc8344c | ||
|
|
65c9e72bf4 | ||
|
|
ea4d954c77 | ||
|
|
43523a96a2 | ||
|
|
2e2fa9e74f | ||
|
|
e43ffb20a1 | ||
|
|
2f0f247e70 | ||
|
|
5bda4f0c26 | ||
|
|
d39c8a3a19 | ||
|
|
e465415039 | ||
|
|
5cef77b8e6 | ||
|
|
60e733c30c | ||
|
|
8b98816eb9 | ||
|
|
50165b3e35 | ||
|
|
0be8b5a5c4 | ||
|
|
451bb6e9db | ||
|
|
83196d4cb5 | ||
|
|
0003e55ad5 | ||
|
|
02014eda6c | ||
|
|
f1c6cd69e9 | ||
|
|
ace281ff6c | ||
|
|
c9edd525e0 | ||
|
|
3f35b442c3 | ||
|
|
87e9365016 | ||
|
|
9806509f4a | ||
|
|
91600a34b6 | ||
|
|
d16351d207 | ||
|
|
4caf638201 | ||
|
|
375fcbd63c | ||
|
|
6ff3a2cf7c | ||
|
|
a890fe3a9a | ||
|
|
2b8bf29fce | ||
|
|
26400a9e4e | ||
|
|
f8b9bb9083 | ||
|
|
42f9047a54 | ||
|
|
6433a3a5f3 | ||
|
|
4b6a03c904 | ||
|
|
ff3df01d98 | ||
|
|
cdc99854b2 | ||
|
|
e7072bcb75 | ||
|
|
7950bde3c6 | ||
|
|
a259669c98 | ||
|
|
603e6be9b4 | ||
|
|
a78c484467 | ||
|
|
e78f07b343 | ||
|
|
8abf10aacb | ||
|
|
2fef4acdd6 | ||
|
|
de27be3a36 | ||
|
|
c62e8539a1 | ||
|
|
22c0aef9c0 | ||
|
|
87805bc36d | ||
|
|
99c4d24eab | ||
|
|
7bf9c4a2d9 | ||
|
|
304e902fce | ||
|
|
0155d93622 | ||
|
|
ebd097bdbe | ||
|
|
a11d01f8a3 | ||
|
|
38491b694b | ||
|
|
e702c1a8ca | ||
|
|
1adea3c678 | ||
|
|
9af812a3e7 | ||
|
|
36bdf8a67e | ||
|
|
20b30fc70a | ||
|
|
e59ff6ca74 | ||
|
|
0e5db36205 | ||
|
|
7960944b14 | ||
|
|
71c2383cbe | ||
|
|
5f5b272726 | ||
|
|
b34fe8f118 | ||
|
|
810be4f6c7 | ||
|
|
1ebbe26ebb | ||
|
|
0f5d5dd2b2 |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.76.0
|
||||
RUSTUP_TOOLCHAIN: 1.77.1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -83,15 +83,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.76.0
|
||||
rust: 1.77.1
|
||||
- os: windows-latest
|
||||
rust: 1.76.0
|
||||
rust: 1.77.1
|
||||
- os: macos-latest
|
||||
rust: 1.76.0
|
||||
rust: 1.77.1
|
||||
|
||||
# Minimum Supported Rust Version = 1.70.0
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.70.0
|
||||
rust: 1.77.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -105,10 +105,20 @@ jobs:
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace --doc
|
||||
|
||||
- name: Test cargo vendor
|
||||
run: cargo vendor
|
||||
|
||||
44
.github/workflows/deltachat-rpc-server.yml
vendored
44
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
# Build a version statically linked against musl libc
|
||||
# to avoid problems with glibc version incompatibility.
|
||||
build_linux:
|
||||
name: Build deltachat-rpc-server for Linux
|
||||
name: Linux
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
name: Build deltachat-rpc-server for Windows
|
||||
name: Windows
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_macos:
|
||||
name: Build deltachat-rpc-server for macOS
|
||||
name: macOS
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -94,6 +94,30 @@ jobs:
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
build_android:
|
||||
name: Android
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [arm64-v8a, armeabi-v7a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
path: result/bin/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
@@ -165,6 +189,18 @@ jobs:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Download Android binary for arm64-v8a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-arm64-v8a-android
|
||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||
|
||||
- name: Download Android binary for armeabi-v7a
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: deltachat-rpc-server-armeabi-v7a-android
|
||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||
|
||||
- name: Create bin/ directory
|
||||
run: |
|
||||
mkdir -p bin
|
||||
@@ -177,6 +213,8 @@ jobs:
|
||||
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
|
||||
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
|
||||
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
|
||||
|
||||
- name: List binaries
|
||||
run: ls -l bin/
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
|
||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to py.delta.chat
|
||||
- name: Upload to c.delta.chat
|
||||
run: |
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||
|
||||
2
.github/workflows/upload-ffi-docs.yml
vendored
2
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_fii` crate documentation
|
||||
# to build `deltachat_ffi` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -44,3 +44,10 @@ node/build/
|
||||
node/dist/
|
||||
node/prebuilds/
|
||||
node/.nyc_output/
|
||||
|
||||
# Nix symlink.
|
||||
result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -1,5 +1,112 @@
|
||||
# Changelog
|
||||
|
||||
## [1.137.2] - 2024-04-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Test reactions for forwarded messages
|
||||
|
||||
### Refactor
|
||||
|
||||
- `is_probably_private_reply`: Remove reaction-specific code.
|
||||
- Use Rust 1.77.0 support for recursion in async functions.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
|
||||
- Update chrono from 0.4.34 to 0.4.37.
|
||||
- Update from brotli 3.4.0 to brotli 4.0.0.
|
||||
- Upgrade `h2` from 0.4.3 to 0.4.4.
|
||||
- Upgrade `image` from 0.24.9 to 0.25.1.
|
||||
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
|
||||
|
||||
## [1.137.1] - 2024-04-03
|
||||
|
||||
### CI
|
||||
|
||||
- Remove android builds for `x86` and `x86_64`.
|
||||
|
||||
## [1.137.0] - 2024-04-02
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`.
|
||||
- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)).
|
||||
- deltachat-rpc-client: Add futures.
|
||||
|
||||
### Build system
|
||||
|
||||
- cmake: Build outside the source tree.
|
||||
- nix: Add outputs for Android binaries.
|
||||
- Add `repository` to Cargo.toml.
|
||||
- python: Remove `setuptools_scm` dependency.
|
||||
- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update to Rust 1.77.0.
|
||||
- Build deltachat-rpc-server for Android.
|
||||
- Shorter names for deltachat-rpc-server jobs.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not include provider hostname in `Message-ID`.
|
||||
- Include 3 recent Message-IDs in `References` header.
|
||||
- Include more entries into DNS fallback cache.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)).
|
||||
- Rescan folders after changing `Config::SentboxWatch`.
|
||||
- Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`.
|
||||
- Put overridden sender name into message info.
|
||||
- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)).
|
||||
- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `MessageObject::from_message_id()`.
|
||||
- jsonrpc: Add `msg_id` and `account_id` to `get_message()` errors.
|
||||
- Cleanup `jobs` and `Params` relicts.
|
||||
|
||||
### Tests
|
||||
|
||||
- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
|
||||
- Remove flaky time check from `test_list_from()`.
|
||||
- Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Add `result` to .gitignore.
|
||||
- cargo: Bump thiserror from 1.0.57 to 1.0.58.
|
||||
- cargo: Bump tokio from 1.36.0 to 1.37.0.
|
||||
- cargo: Bump pin-project from 1.1.4 to 1.1.5.
|
||||
- cargo: Bump strum from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump uuid from 1.7.0 to 1.8.0.
|
||||
- cargo: Bump toml from 0.8.10 to 0.8.12.
|
||||
- cargo: Bump tokio-stream from 0.1.14 to 0.1.15.
|
||||
- cargo: Bump smallvec from 1.13.1 to 1.13.2.
|
||||
- cargo: Bump async-smtp from 0.9.0 to 0.9.1.
|
||||
- cargo: Bump strum_macros from 0.26.1 to 0.26.2.
|
||||
- cargo: Bump serde_json from 1.0.114 to 1.0.115.
|
||||
- cargo: Bump anyhow from 1.0.80 to 1.0.81.
|
||||
- cargo: Bump syn from 2.0.52 to 2.0.57.
|
||||
- cargo: Bump futures-lite from 2.2.0 to 2.3.0.
|
||||
- cargo: Bump axum from 0.7.4 to 0.7.5.
|
||||
- cargo: Bump reqwest from 0.11.24 to 0.12.2.
|
||||
- cargo: Bump backtrace from 0.3.69 to 0.3.71.
|
||||
- cargo: Bump regex from 1.10.3 to 1.10.4.
|
||||
- cargo: Update aho-corasick from 1.1.2 to 1.1.3.
|
||||
- Update deny.toml.
|
||||
|
||||
## [1.136.6] - 2024-03-19
|
||||
|
||||
### Build system
|
||||
@@ -3762,3 +3869,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
|
||||
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
|
||||
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
|
||||
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
|
||||
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
|
||||
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
|
||||
|
||||
@@ -12,26 +12,22 @@ else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||
else()
|
||||
set(ARCH_DIR "./")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --release --no-default-features --features jsonrpc
|
||||
|
||||
# Build in `deltachat-ffi` directory instead of using
|
||||
# `--package deltachat_ffi` to avoid feature resolver version
|
||||
# "1" bug which makes `--no-default-features` affect only
|
||||
# `deltachat`, but not `deltachat-ffi` package.
|
||||
#
|
||||
# We can't enable version "2" resolver [1] because it is not
|
||||
# stable yet on rust 1.50.0.
|
||||
#
|
||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
@@ -39,12 +35,12 @@ add_custom_target(
|
||||
lib_deltachat
|
||||
ALL
|
||||
DEPENDS
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
467
Cargo.lock
generated
467
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
46
Cargo.toml
46
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.136.6"
|
||||
version = "1.137.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -33,32 +34,33 @@ strip = true
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
anyhow = { workspace = true }
|
||||
async-channel = "2.0.0"
|
||||
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
brotli = { version = "3.4", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
base64 = "0.22"
|
||||
brotli = { version = "5", default-features=false, features = ["std"] }
|
||||
chrono = { workspace = true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.2.0"
|
||||
futures-lite = "2.3.0"
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.24.9", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.2", default-features = false }
|
||||
kamadak-exif = "0.5"
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
mailparse = "0.14"
|
||||
@@ -66,7 +68,7 @@ mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
@@ -76,24 +78,24 @@ qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.11.24", features = ["json"] }
|
||||
rusqlite = { version = "0.31", features = ["sqlcipher"] }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.8"
|
||||
@@ -112,13 +114,13 @@ openssl-src = "~300.1"
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.2.0"
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.9.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
@@ -131,6 +133,7 @@ members = [
|
||||
"deltachat-repl",
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -161,6 +164,13 @@ harness = false
|
||||
name = "send_events"
|
||||
harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
once_cell = "1.18.0"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.31"
|
||||
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -192,6 +195,7 @@ or its language bindings:
|
||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
20
deltachat-contact-tools/Cargo.toml
Normal file
20
deltachat-contact-tools/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
edition = "2021"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||
license = "MPL-2.0"
|
||||
# TODO maybe it should be called "deltachat-text-utils" or similar?
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true }
|
||||
ical = { version = "0.11.0", default-features = false, features = ["vcard"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
453
deltachat-contact-tools/src/lib.rs
Normal file
453
deltachat-contact-tools/src/lib.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
unused,
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
clippy::all,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless,
|
||||
clippy::unused_async,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string,
|
||||
clippy::bool_to_int_with_if
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// TODOs to clean up:
|
||||
// - Check if sanitizing is done correctly everywhere
|
||||
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Contact {
|
||||
addr: String,
|
||||
display_name: String,
|
||||
key: String,
|
||||
profile_photo: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn parse_vcard(vcard: String) -> Result<Vec<Contact>> {
|
||||
let reader = ical::VcardParser::new(vcard.as_bytes());
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for vcard_contact in reader {
|
||||
let vcard_contact = vcard_contact?; // TODO should just continue with the next contact
|
||||
let mut new_contact = Contact::default();
|
||||
|
||||
let mut display_name = None;
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut timestamp = None;
|
||||
|
||||
for property in vcard_contact.properties {
|
||||
match &*property.name.to_lowercase() {
|
||||
"email" => addr = addr.or(property.value),
|
||||
"fn" => display_name = display_name.or(property.value),
|
||||
"key" => key = key.or(dbg!(property).value), // TODO hmmm, the ical crate can apparently only parse version 3.0
|
||||
"photo" => photo = photo.or(property.value),
|
||||
"rev" => {
|
||||
timestamp = timestamp.or(property.value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (display_name, addr) =
|
||||
sanitize_name_and_addr(&display_name.unwrap_or_default(), &addr.unwrap_or_default());
|
||||
new_contact.display_name = display_name;
|
||||
new_contact.addr = addr;
|
||||
|
||||
if let Some(key) = key {
|
||||
if let Some(base64_key) = key
|
||||
.strip_prefix("PGP;ENCODING=BASE64:")
|
||||
.or(key.strip_prefix("TYPE=PGP;ENCODING=b:"))
|
||||
.or(key.strip_prefix("data:application/pgp-keys;base64,"))
|
||||
{
|
||||
new_contact.key = base64_key.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(photo) = photo {
|
||||
if let Some(base64_photo) = photo
|
||||
.strip_prefix("PGP;ENCODING=BASE64:")
|
||||
.or(photo.strip_prefix("TYPE=PGP;ENCODING=b:"))
|
||||
.or(photo.strip_prefix("data:application/pgp-keys;base64,"))
|
||||
{}
|
||||
}
|
||||
|
||||
contacts.push(new_contact);
|
||||
}
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the name and address
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
strip_rtlo_characters(&normalize_name(name)),
|
||||
addr.to_string(),
|
||||
)
|
||||
};
|
||||
let mut name = normalize_name(&name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
}
|
||||
|
||||
(name, addr)
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurrences of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
///
|
||||
/// Represents an email address, right now just the `name@domain` portion.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use deltachat_contact_tools::EmailAddress;
|
||||
/// let email = match EmailAddress::new("someone@example.com") {
|
||||
/// Ok(addr) => addr,
|
||||
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
||||
/// };
|
||||
/// assert_eq!(&email.local, "someone");
|
||||
/// assert_eq!(&email.domain, "example.com");
|
||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct EmailAddress {
|
||||
/// Local part of the email address.
|
||||
pub local: String,
|
||||
|
||||
/// Email address domain.
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}@{}", self.local, self.domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
/// Performs a dead-simple parse of an email address.
|
||||
pub fn new(input: &str) -> Result<EmailAddress> {
|
||||
if input.is_empty() {
|
||||
bail!("empty string is not valid");
|
||||
}
|
||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||
|
||||
if input
|
||||
.chars()
|
||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||
{
|
||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||
}
|
||||
|
||||
match &parts[..] {
|
||||
[domain, local] => {
|
||||
if local.is_empty() {
|
||||
bail!("empty string is not valid for local part in {:?}", input);
|
||||
}
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
})
|
||||
}
|
||||
_ => bail!("Email {:?} must contain '@' character", input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_thunderbird() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'Alice Mueller'
|
||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'bobzzz@freenet.de'
|
||||
EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, "".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].display_name, "".to_string());
|
||||
assert_eq!(contacts[1].key, "".to_string());
|
||||
assert_eq!(contacts[1].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_example() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Alice Wonderland
|
||||
N:Wonderland;Alice;;;Ms.
|
||||
GENDER:W
|
||||
EMAIL;TYPE=work:alice@example.com
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, "[base64-data]".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
assert_eq!(contacts[0].timestamp, 1713465762); // I did not check whether this timestamp is correct
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emailaddress_parse() {
|
||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@domain.tld").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "domain.tld".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@localhost").unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "localhost".into()
|
||||
}
|
||||
);
|
||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||
assert!(EmailAddress::new("u@d").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_err());
|
||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
EmailAddress {
|
||||
local: "u".into(),
|
||||
domain: "d.tt".into(),
|
||||
}
|
||||
);
|
||||
assert!(EmailAddress::new("u@tt").is_ok());
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_android_contact_export() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
TEL;CELL:+1-234-567-890
|
||||
EMAIL;HOME:bob@example.org
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Alice;;;
|
||||
FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, "".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].display_name, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, "".to_string());
|
||||
assert_eq!(contacts[1].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.136.6"
|
||||
version = "1.137.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -20,7 +20,7 @@ libc = "0.2"
|
||||
human-panic = { version = "1", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
|
||||
@@ -17,7 +17,6 @@ typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_reactions dc_reactions_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
@@ -705,7 +704,6 @@ int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Standalone version of dc_accounts_all_work_done().
|
||||
* Only used by the python tests.
|
||||
*/
|
||||
int dc_all_work_done (dc_context_t* context);
|
||||
@@ -1118,36 +1116,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `send_reaction` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id ID of the message you react to.
|
||||
* @param reaction A string consisting of emojis separated by spaces.
|
||||
* @return The ID of the message sent out or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
||||
|
||||
|
||||
/**
|
||||
* Get a structure with reactions to the message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to get reactions for.
|
||||
* @return A structure with all reactions to the message.
|
||||
*/
|
||||
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -3099,23 +3067,6 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
|
||||
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||
|
||||
|
||||
/**
|
||||
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
*
|
||||
* iOS can:
|
||||
* - call dc_start_io() (in case IO was not running)
|
||||
* - call dc_maybe_network()
|
||||
* - while dc_accounts_all_work_done() returns false:
|
||||
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Whether all accounts finished their background work.
|
||||
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
*/
|
||||
int dc_accounts_all_work_done (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
||||
* If IO is already running, nothing happens.
|
||||
@@ -5338,52 +5289,6 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_reactions_t
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
*
|
||||
* An object representing all reactions for a single message.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns array of contacts which reacted to the given message.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
||||
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
||||
*/
|
||||
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string containing space-separated reactions of a single contact.
|
||||
*
|
||||
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @param contact_id ID of the contact.
|
||||
* @return Space-separated list of emoji sequences, which could be empty.
|
||||
* Returned string should not be modified and should be freed
|
||||
* with dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Frees an object containing message reactions.
|
||||
*
|
||||
* Reactions objects are created by dc_get_msg_reactions().
|
||||
*
|
||||
* @deprecated 2023-11-27
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object to free.
|
||||
* If NULL is given, nothing is done.
|
||||
*/
|
||||
void dc_reactions_unref (dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_MSG DC_MSG
|
||||
*
|
||||
@@ -6074,10 +5979,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Downloading a bunch of messages just finished. This is an
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
|
||||
* UI may store #DC_EVENT_INCOMING_MSG events
|
||||
* and display notifications for all messages at once
|
||||
* when this event arrives.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) msg_ids, a json object with the message ids.
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
||||
|
||||
@@ -6306,7 +6213,24 @@ void dc_event_unref(dc_event_t* event);
|
||||
* This event is only emitted by the account manager
|
||||
*/
|
||||
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||
|
||||
/**
|
||||
* Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
*
|
||||
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_CHANGED 2300
|
||||
|
||||
/**
|
||||
* Inform that all or a single chat list item changed and needs to be rerendered
|
||||
* If `chat_id` is set to 0, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
*
|
||||
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
|
||||
*/
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -7312,6 +7236,22 @@ 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
|
||||
|
||||
/// "You reacted %1$s to '%2$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%2$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_YOU_REACTED 176
|
||||
|
||||
/// "%1$s reacted %2$s to '%3$s'"
|
||||
///
|
||||
/// `%1$s` will be replaced by the name the contact who reacted
|
||||
/// `%2$s` will be replaced by the reaction, usually an emoji
|
||||
/// `%3$s` will be replaced by the summary of the message the reaction refers to
|
||||
///
|
||||
/// Used in summaries.
|
||||
#define DC_STR_REACTED_BY 177
|
||||
|
||||
/**
|
||||
* @}
|
||||
|
||||
@@ -32,7 +32,6 @@ use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
@@ -68,8 +67,6 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
pub type dc_reactions_t = Reactions;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
@@ -567,6 +564,8 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,6 +595,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -620,6 +620,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +659,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
@@ -719,7 +724,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -731,11 +739,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
|
||||
.unwrap_or_default()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
EventType::ConfigSynced { key } => {
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
@@ -1019,49 +1022,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_reaction(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
reaction: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_reaction()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_reactions_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||
.context("failed dc_get_msg_reactions() call")
|
||||
.log_err(ctx)
|
||||
{
|
||||
reactions
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(reactions))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -4248,45 +4208,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
||||
lot.get_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
||||
reactions: *mut dc_reactions_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
let array: dc_array_t = reactions.contacts().into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
||||
reactions: *mut dc_reactions_t,
|
||||
contact_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_unref()");
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(reactions));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
@@ -4855,16 +4776,6 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
|
||||
return 0;
|
||||
}
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.136.6"
|
||||
version = "1.137.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
@@ -24,17 +25,17 @@ futures = { version = "0.3.30" }
|
||||
serde_json = "1"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.33.0" }
|
||||
tokio = { version = "1.37.0" }
|
||||
sanitize-filename = "0.5"
|
||||
walkdir = "2.5.0"
|
||||
base64 = "0.21"
|
||||
base64 = "0.22"
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
env_logger = { version = "0.11.3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
@@ -62,14 +63,14 @@ use crate::api::types::qr::QrObject;
|
||||
struct AccountState {
|
||||
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
||||
///
|
||||
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Pending` or `Ready`, otherwise `NoProvider`.
|
||||
backup_provider_qr: watch::Sender<ProviderQr>,
|
||||
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
|
||||
/// `Some`, otherwise `None`.
|
||||
backup_provider_qr: watch::Sender<Option<Qr>>,
|
||||
}
|
||||
|
||||
impl Default for AccountState {
|
||||
fn default() -> Self {
|
||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
||||
let tx = watch::Sender::new(None);
|
||||
Self {
|
||||
backup_provider_qr: tx,
|
||||
}
|
||||
@@ -123,21 +124,13 @@ impl CommandApi {
|
||||
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
||||
.await;
|
||||
|
||||
let val: ProviderQr = receiver.borrow_and_update().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => loop {
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
let val: ProviderQr = receiver.borrow().clone();
|
||||
match val {
|
||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
||||
ProviderQr::Pending => continue,
|
||||
ProviderQr::Ready(qr) => break Ok(qr),
|
||||
};
|
||||
},
|
||||
ProviderQr::Ready(qr) => Ok(qr),
|
||||
loop {
|
||||
if let Some(qr) = receiver.borrow_and_update().clone() {
|
||||
return Ok(qr);
|
||||
}
|
||||
if receiver.changed().await.is_err() {
|
||||
bail!("No backup being provided (account state dropped)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1097,9 +1090,12 @@ impl CommandApi {
|
||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||
}
|
||||
|
||||
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
|
||||
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
MessageObject::from_message_id(&ctx, message_id).await
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
MessageObject::from_msg_id(&ctx, msg_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||
}
|
||||
|
||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||
@@ -1119,7 +1115,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
||||
for message_id in message_ids {
|
||||
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
|
||||
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
|
||||
messages.insert(
|
||||
message_id,
|
||||
match message_result {
|
||||
@@ -1566,20 +1562,21 @@ impl CommandApi {
|
||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(ProviderQr::Pending);
|
||||
})
|
||||
.await;
|
||||
|
||||
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
||||
self.with_state(account_id, |state| {
|
||||
state
|
||||
.backup_provider_qr
|
||||
.send_replace(ProviderQr::Ready(provider.qr()));
|
||||
state.backup_provider_qr.send_replace(Some(provider.qr()));
|
||||
})
|
||||
.await;
|
||||
|
||||
provider.await
|
||||
let res = provider.await;
|
||||
|
||||
self.with_state(account_id, |state| {
|
||||
state.backup_provider_qr.send_replace(None);
|
||||
})
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||
@@ -1587,11 +1584,17 @@ impl CommandApi {
|
||||
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
qr::format_backup(&qr)
|
||||
}
|
||||
|
||||
@@ -1600,14 +1603,20 @@ impl CommandApi {
|
||||
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
||||
/// retrieve the backup and setup this second device.
|
||||
///
|
||||
/// This call will fail if there is currently no concurrent call to
|
||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
||||
/// ready.
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
.context("Backup provider did not start in time")?
|
||||
.context("Failed to get backup QR code")?;
|
||||
generate_backup_qr(&ctx, &qr).await
|
||||
}
|
||||
|
||||
@@ -1617,6 +1626,9 @@ impl CommandApi {
|
||||
/// the current device.
|
||||
///
|
||||
/// Can be cancelled by stopping the ongoing process.
|
||||
///
|
||||
/// Do not forget to call start_io on the account after a successful import,
|
||||
/// otherwise it will not connect to the email server.
|
||||
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
||||
@@ -2042,11 +2054,9 @@ impl CommandApi {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.to_u32();
|
||||
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id, message))
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||
Ok((msg_id.to_u32(), message))
|
||||
}
|
||||
|
||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||
@@ -2140,15 +2150,3 @@ async fn get_config(
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a QR code for a BackupProvider is currently available.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum ProviderQr {
|
||||
/// There is no provider, asking for a QR is an error.
|
||||
NoProvider,
|
||||
/// There is a provider, the QR code is pending.
|
||||
Pending,
|
||||
/// There is a provider and QR code.
|
||||
Ready(Qr),
|
||||
}
|
||||
|
||||
@@ -101,17 +101,15 @@ pub enum EventType {
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||
|
||||
/// Downloading a bunch of messages just finished. This is an experimental
|
||||
/// Downloading a bunch of messages just finished. This is an
|
||||
/// event to allow the UI to only show one notification per message bunch,
|
||||
/// instead of cluttering the user with many notifications.
|
||||
///
|
||||
/// msg_ids contains the message ids.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsgBunch { msg_ids: Vec<u32> },
|
||||
IncomingMsgBunch,
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
@@ -252,6 +250,15 @@ pub enum EventType {
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
///
|
||||
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||
ChatlistChanged,
|
||||
|
||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -287,9 +294,7 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||
},
|
||||
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
|
||||
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
@@ -361,6 +366,10 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +105,6 @@ enum MessageQuote {
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
||||
let msg_id = MsgId::new(message_id);
|
||||
Self::from_msg_id(context, msg_id).await
|
||||
}
|
||||
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.136.6"
|
||||
"version": "1.137.2"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.136.6"
|
||||
version = "1.137.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
@@ -12,8 +13,8 @@ dirs = "5"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.31"
|
||||
rustyline = "13"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -33,14 +33,6 @@ use tokio::fs;
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
}
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
||||
requires = ["setuptools>=45"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.137.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -19,10 +20,10 @@ classifiers = [
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"imap-tools",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
@@ -32,9 +33,6 @@ deltachat_rpc_client = [
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
|
||||
@@ -104,7 +104,11 @@ def _run_cli(
|
||||
if not client.is_configured():
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread = Thread(
|
||||
target=client.configure,
|
||||
daemon=True,
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
@@ -168,3 +172,33 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
return "removed", addr, addr
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class futuremethod: # noqa: N801
|
||||
"""Decorator for async methods."""
|
||||
|
||||
def __init__(self, func):
|
||||
self._func = func
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
def future(*args):
|
||||
generator = self._func(instance, *args)
|
||||
res = next(generator)
|
||||
|
||||
def f():
|
||||
try:
|
||||
generator.send(res())
|
||||
except StopIteration as e:
|
||||
return e.value
|
||||
|
||||
return f
|
||||
|
||||
def wrapper(*args):
|
||||
f = future(*args)
|
||||
return f()
|
||||
|
||||
wrapper.future = future
|
||||
return wrapper
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
@@ -28,6 +30,10 @@ class Account:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
self._rpc.clear_all_events(self.id)
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
@@ -76,9 +82,24 @@ class Account:
|
||||
"""Get self avatar."""
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
def configure(self) -> None:
|
||||
def check_qr(self, qr):
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
self._rpc.set_config_from_qr(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def configure(self):
|
||||
"""Configure an account."""
|
||||
self._rpc.configure(self.id)
|
||||
yield self._rpc.configure.future(self.id)
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
@@ -97,6 +118,11 @@ class Account:
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
addr = account.get_config("addr")
|
||||
contact = self.create_contact(addr)
|
||||
return contact.create_chat()
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
@@ -106,7 +132,7 @@ class Account:
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
def get_blocked_contacts(self) -> list[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
@@ -131,7 +157,7 @@ class Account:
|
||||
with_self: bool = False,
|
||||
verified_only: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[List[Contact], List[AttrDict]]:
|
||||
) -> Union[list[Contact], list[AttrDict]]:
|
||||
"""Get a filtered list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
@@ -166,7 +192,7 @@ class Account:
|
||||
no_specials: bool = False,
|
||||
alldone_hint: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[List[Chat], List[AttrDict]]:
|
||||
) -> Union[list[Chat], list[AttrDict]]:
|
||||
"""Return list of chats.
|
||||
|
||||
:param query: if a string is specified only chats matching this query are returned.
|
||||
@@ -224,7 +250,7 @@ class Account:
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
@@ -236,15 +262,15 @@ class Account:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
def mark_seen_messages(self, messages: list[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
def delete_messages(self, messages: list[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
def get_fresh_messages(self) -> list[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
@@ -254,12 +280,12 @@ class Account:
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
def get_next_messages(self) -> list[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
@@ -283,7 +309,13 @@ class Account:
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
def wait_for_reactions_changed(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.REACTIONS_CHANGED:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import ChatVisibility, ViewType
|
||||
@@ -93,7 +95,7 @@ class Chat:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
@@ -117,7 +119,7 @@ class Chat:
|
||||
html: Optional[str] = None,
|
||||
viewtype: Optional[ViewType] = None,
|
||||
file: Optional[str] = None,
|
||||
location: Optional[Tuple[float, float]] = None,
|
||||
location: Optional[tuple[float, float]] = None,
|
||||
override_sender_name: Optional[str] = None,
|
||||
quoted_msg: Optional[Union[int, Message]] = None,
|
||||
) -> Message:
|
||||
@@ -142,6 +144,10 @@ class Chat:
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_file(self, path):
|
||||
"""Send a file and return the resulting Message instance."""
|
||||
return self.send_message(file=path)
|
||||
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
@@ -152,7 +158,7 @@ class Chat:
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
def forward_messages(self, messages: list[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
@@ -184,7 +190,7 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
@@ -219,7 +225,7 @@ class Chat:
|
||||
contact_id = cnt
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
def get_contacts(self) -> list[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
@@ -243,7 +249,7 @@ class Chat:
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
timestamp_to: Optional["datetime"] = None,
|
||||
) -> List[AttrDict]:
|
||||
) -> list[AttrDict]:
|
||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
@@ -251,7 +257,7 @@ class Chat:
|
||||
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
contacts: dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
location = AttrDict(loc)
|
||||
location["chat"] = self
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
@@ -39,16 +38,16 @@ class Client:
|
||||
def __init__(
|
||||
self,
|
||||
account: "Account",
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
) -> None:
|
||||
self.account = account
|
||||
self.logger = logger or logging
|
||||
self._hooks: Dict[type, Set[tuple]] = {}
|
||||
self._hooks: dict[type, set[tuple]] = {}
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .account import Account
|
||||
@@ -21,7 +23,7 @@ class DeltaChat:
|
||||
account_id = self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
def get_all_accounts(self) -> list[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
@@ -44,6 +46,6 @@ class DeltaChat:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Internal Python-level IMAP handling used by the tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
return self.conn.folder.set(foldername)
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None.
|
||||
"""
|
||||
if "_" not in config_name:
|
||||
config_name = f"configured_{config_name}_folder"
|
||||
foldername = self.account.get_config(config_name)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
return None
|
||||
|
||||
def list_folders(self) -> list[str]:
|
||||
"""return list of all existing folder names."""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
"""delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self) -> list[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch())
|
||||
|
||||
def get_unread_messages(self) -> list[str]:
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
assert not self._idling
|
||||
stream = io.StringIO()
|
||||
|
||||
def log(*args, **kwargs):
|
||||
kwargs["file"] = stream
|
||||
print(*args, **kwargs)
|
||||
|
||||
empty_folders = []
|
||||
for imapfolder in self.list_folders():
|
||||
self.select_folder(imapfolder)
|
||||
messages = list(self.get_all_messages())
|
||||
if not messages:
|
||||
empty_folders.append(imapfolder)
|
||||
continue
|
||||
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
for msg in self.conn.fetch(mark_seen=False):
|
||||
body = getattr(msg.obj, "text", None)
|
||||
if not body:
|
||||
body = getattr(msg.obj, "html", None)
|
||||
if not body:
|
||||
log("Message", msg.uid, "has empty body")
|
||||
continue
|
||||
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log(
|
||||
"Message",
|
||||
msg.uid,
|
||||
msg.flags,
|
||||
"Message-Id:",
|
||||
msg.obj.get("Message-Id"),
|
||||
)
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
"""return Idle ContextManager."""
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
finally:
|
||||
idle_manager.done()
|
||||
|
||||
def append(self, folder: str, msg: str):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> list[bytes]:
|
||||
"""(blocking) wait for next idle message from server."""
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||
return res
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b"EXISTS" in item or b"RECENT" in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
"""Return first message with SEEN flag from a running idle-stream."""
|
||||
while True:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb"\Seen" in item:
|
||||
return int(item.split(b" ")[1])
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
@@ -1,8 +1,10 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
||||
|
||||
from .const import EventType
|
||||
|
||||
@@ -263,9 +265,9 @@ class HookCollection:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
||||
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
||||
return iter(self._hooks)
|
||||
|
||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||
|
||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -21,9 +22,10 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
def send_reaction(self, *reaction: str):
|
||||
def send_reaction(self, *reaction: str) -> "Message":
|
||||
"""Send a reaction to this message."""
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
@@ -61,3 +63,10 @@ class Message:
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
def wait_until_delivered(self) -> None:
|
||||
"""Consume events until the message is delivered."""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
@@ -37,9 +40,10 @@ class ACFactory:
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_account(self) -> Account:
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
account = self.new_preconfigured_account()
|
||||
account.configure()
|
||||
yield account.configure.future()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
@@ -49,17 +53,15 @@ class ACFactory:
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
@futuremethod
|
||||
def get_online_account(self):
|
||||
account = yield self.new_configured_account.future()
|
||||
account.bring_online()
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
def get_online_accounts(self, num: int) -> list[Account]:
|
||||
futures = [self.get_online_account.future() for _ in range(num)]
|
||||
return [f() for f in futures]
|
||||
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
@@ -71,6 +73,10 @@ class ACFactory:
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
|
||||
@@ -1,18 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from queue import Empty, Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
def __init__(self, rpc: "Rpc", name: str):
|
||||
self.rpc = rpc
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
"""Synchronously calls JSON-RPC method."""
|
||||
future = self.future(*args)
|
||||
return future()
|
||||
|
||||
def future(self, *args) -> Any:
|
||||
"""Asynchronously calls JSON-RPC method."""
|
||||
request_id = next(self.rpc.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": self.name,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.rpc.request_events[request_id] = event
|
||||
self.rpc.request_queue.put(request)
|
||||
|
||||
return RpcFuture(self.rpc, request_id, event)
|
||||
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
@@ -25,11 +69,11 @@ class Rpc:
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
self.event_queues: dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
self.request_events: dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_results: dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_thread: Thread
|
||||
@@ -144,25 +188,14 @@ class Rpc:
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def clear_all_events(self, account_id: int):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
queue = self.get_queue(account_id)
|
||||
try:
|
||||
while True:
|
||||
queue.get_nowait()
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def method(*args) -> Any:
|
||||
request_id = next(self.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
return method
|
||||
return RpcMethod(self, attr)
|
||||
|
||||
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||
|
||||
|
||||
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||
first_event = ""
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
first_event = "change"
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
first_event = "item_change"
|
||||
break
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
|
||||
break
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist_specific_item(account, chat_id):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||
break
|
||||
|
||||
|
||||
def wait_for_chatlist(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.CHATLIST_CHANGED:
|
||||
break
|
||||
|
||||
|
||||
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes (delivered, read)
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
alice.clear_all_events()
|
||||
bob.stop_io()
|
||||
alice.stop_io()
|
||||
alice_chat_bob.send_text("hi")
|
||||
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
alice.clear_all_events()
|
||||
alice.start_io()
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
|
||||
bob.clear_all_events()
|
||||
bob.start_io()
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg.get_snapshot().chat.accept()
|
||||
msg.mark_seen()
|
||||
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_READ:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||
|
||||
|
||||
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test change status on chatlistitem when status changes failed
|
||||
"""
|
||||
(alice,) = acfactory.get_online_accounts(1)
|
||||
|
||||
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
failing_message = invalid_chat.send_text("test")
|
||||
|
||||
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSG_FAILED:
|
||||
break
|
||||
|
||||
wait_for_chatlist_specific_item(alice, invalid_chat.id)
|
||||
|
||||
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||
|
||||
|
||||
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test if download on demand emits chatlist update events.
|
||||
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
"Hello World, this message is bigger than 5 bytes",
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
bob.wait_for_incoming_msg_event()
|
||||
|
||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||
|
||||
alice._rpc.provide_backup.future(alice.id)
|
||||
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||
alice_second_device.start_io()
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.clear_all_events()
|
||||
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||
|
||||
|
||||
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test that chatlist changed events are emitted for the second device
|
||||
when the message is marked as read on the first device
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
bob.get_chat_by_id(bob_chat_id).send_text("hello")
|
||||
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
wait_for_chatlist_specific_item(bob, bob_chat_id)
|
||||
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||
|
||||
|
||||
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||
"""
|
||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||
|
||||
alice_chat_bob.archive()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.pin()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.mute()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import Chat, SpecialContactId
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
@@ -579,3 +579,40 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
|
||||
def test_withdraw_securejoin_qr(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
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, _svg = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
qr = alice.check_qr(qr_code)
|
||||
assert qr["kind"] == "withdrawVerifyGroup"
|
||||
alice.set_config_from_qr(qr_code)
|
||||
|
||||
logging.info("Bob scans withdrawn QR code.")
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
|
||||
logging.info("Bob scanned withdrawn QR code")
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||
break
|
||||
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -439,3 +444,143 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
message = alice.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.show_padlock
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
m.wait_until_delivered()
|
||||
|
||||
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
msgs[-1].wait_until_delivered()
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
logging.info("wait for ac2 to receive a reaction")
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1_addr
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_preconfigured_account()
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
ac2.configure()
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
logging.info("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
msg1.wait_until_delivered()
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msg1.send_reaction(react_str).wait_until_delivered()
|
||||
|
||||
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2_direct_imap = DirectImap(ac2)
|
||||
ac2_direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
logging.info("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
download_limit = 300000
|
||||
|
||||
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||
bob = others[0]
|
||||
|
||||
alice_group = alice.create_group("test group")
|
||||
for account in others:
|
||||
chat = account.create_chat(alice)
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact_addr = account.get_config("addr")
|
||||
contact = alice.create_contact(contact_addr, "")
|
||||
|
||||
alice_group.add_contact(contact)
|
||||
|
||||
if n_accounts == 2:
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
bob.set_config("download_limit", str(download_limit))
|
||||
|
||||
alice_group.send_text("hi")
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "hi"
|
||||
bob_group = snapshot.chat
|
||||
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
|
||||
for i in range(10):
|
||||
logging.info("Sending message %s", i)
|
||||
alice_group.send_file(str(path))
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||
if n_accounts > 2:
|
||||
assert snapshot.chat == bob_group
|
||||
else:
|
||||
# Group contains only Alice and Bob,
|
||||
# so partially downloaded messages are
|
||||
# hard to distinguish from private replies to group messages.
|
||||
#
|
||||
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
|
||||
assert snapshot.chat == bob_chat_alice
|
||||
|
||||
@@ -25,7 +25,7 @@ deps =
|
||||
black
|
||||
commands =
|
||||
black --quiet --check --diff src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
ruff check src/ examples/ tests/
|
||||
|
||||
[pytest]
|
||||
timeout = 300
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.136.6"
|
||||
version = "1.137.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -14,12 +14,12 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "2.2.0"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.33.0", features = ["io-std"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
@@ -37,12 +38,9 @@ skip = [
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "h2", version = "0.3.22" },
|
||||
{ name = "http-body", version = "0.4.5" },
|
||||
{ name = "http", version = "0.2.11" },
|
||||
{ name = "hyper", version = "0.14.27" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
@@ -59,6 +57,7 @@ skip = [
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
|
||||
137
flake.lock
generated
137
flake.lock
generated
@@ -1,8 +1,50 @@
|
||||
{
|
||||
"nodes": {
|
||||
"android": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710633978,
|
||||
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708939976,
|
||||
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
@@ -23,6 +65,42 @@
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
@@ -39,7 +117,7 @@
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
@@ -71,6 +149,22 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1710631334,
|
||||
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
|
||||
@@ -86,7 +180,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1710765496,
|
||||
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
|
||||
@@ -100,7 +194,7 @@
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1710631334,
|
||||
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
|
||||
@@ -118,11 +212,12 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"android": "android",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"naersk": "naersk",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
@@ -156,6 +251,36 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
94
flake.nix
94
flake.nix
@@ -6,8 +6,9 @@
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix }:
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
@@ -15,6 +16,11 @@
|
||||
fenixPkgs = fenix.packages.${system};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -105,8 +111,6 @@
|
||||
mkWin64RustPackage = packageName:
|
||||
let
|
||||
rustTarget = "x86_64-pc-windows-gnu";
|
||||
in
|
||||
let
|
||||
toolchainWin = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
@@ -173,13 +177,12 @@
|
||||
model = "win32";
|
||||
package = null;
|
||||
};
|
||||
})).overrideAttrs (oldAttr: rec{
|
||||
})).overrideAttrs (oldAttr: {
|
||||
configureFlags = oldAttr.configureFlags ++ [
|
||||
"--disable-sjlj-exceptions --with-dwarf2"
|
||||
];
|
||||
})
|
||||
);
|
||||
winStdenv = pkgsWin32.buildPackages.overrideCC pkgsWin32.stdenv winCC;
|
||||
in
|
||||
naerskWin.buildPackage rec {
|
||||
pname = packageName;
|
||||
@@ -251,6 +254,61 @@
|
||||
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||
};
|
||||
|
||||
androidAttrs = {
|
||||
armeabi-v7a = {
|
||||
cc = "armv7a-linux-androideabi19-clang";
|
||||
rustTarget = "armv7-linux-androideabi";
|
||||
};
|
||||
arm64-v8a = {
|
||||
cc = "aarch64-linux-android21-clang";
|
||||
rustTarget = "aarch64-linux-android";
|
||||
};
|
||||
};
|
||||
|
||||
mkAndroidRustPackage = arch: packageName:
|
||||
let
|
||||
rustTarget = androidAttrs.${arch}.rustTarget;
|
||||
toolchain = fenixPkgs.combine [
|
||||
fenixPkgs.stable.rustc
|
||||
fenixPkgs.stable.cargo
|
||||
fenixPkgs.targets.${rustTarget}.stable.rust-std
|
||||
];
|
||||
naersk-lib = pkgs.callPackage naersk {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
};
|
||||
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
|
||||
targetCcName = androidAttrs.${arch}.cc;
|
||||
targetCc = "${targetToolchain}/bin/${targetCcName}";
|
||||
in
|
||||
naersk-lib.buildPackage rec {
|
||||
pname = packageName;
|
||||
cargoBuildOptions = x: x ++ [ "--package" packageName ];
|
||||
version = manifest.version;
|
||||
strictDeps = true;
|
||||
src = rustSrc;
|
||||
nativeBuildInputs = [
|
||||
pkgs.perl # Needed to build vendored OpenSSL.
|
||||
];
|
||||
auditable = false; # Avoid cargo-auditable failures.
|
||||
doCheck = false; # Disable test as it requires network access.
|
||||
|
||||
CARGO_BUILD_TARGET = rustTarget;
|
||||
TARGET_CC = "${targetCc}";
|
||||
CARGO_BUILD_RUSTFLAGS = [
|
||||
"-C"
|
||||
"linker=${TARGET_CC}"
|
||||
];
|
||||
|
||||
CC = "${targetCc}";
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch: {
|
||||
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
let
|
||||
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
|
||||
@@ -295,7 +353,11 @@
|
||||
mkRustPackages "i686-linux" //
|
||||
mkRustPackages "x86_64-linux" //
|
||||
mkRustPackages "armv7l-linux" //
|
||||
mkRustPackages "armv6l-linux" // rec {
|
||||
mkRustPackages "armv6l-linux" //
|
||||
mkAndroidPackages "armeabi-v7a" //
|
||||
mkAndroidPackages "arm64-v8a" //
|
||||
mkAndroidPackages "x86" //
|
||||
mkAndroidPackages "x86_64" // rec {
|
||||
# Run with `nix run .#deltachat-repl foo.db`.
|
||||
deltachat-repl = mkRustPackage "deltachat-repl";
|
||||
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
|
||||
@@ -371,7 +433,7 @@
|
||||
};
|
||||
|
||||
libdeltachat =
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "libdeltachat";
|
||||
version = manifest.version;
|
||||
src = rustSrc;
|
||||
@@ -413,19 +475,18 @@
|
||||
};
|
||||
|
||||
deltachat-rpc-client =
|
||||
pkgs.python3Packages.buildPythonPackage rec {
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-rpc-client";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
|
||||
format = "pyproject";
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.setuptools_scm
|
||||
];
|
||||
};
|
||||
|
||||
deltachat-python =
|
||||
pkgs.python3Packages.buildPythonPackage rec {
|
||||
pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "deltachat-python";
|
||||
version = manifest.version;
|
||||
src = pkgs.lib.cleanSource ./python;
|
||||
@@ -438,7 +499,6 @@
|
||||
];
|
||||
propagatedBuildInputs = [
|
||||
pkgs.python3Packages.setuptools
|
||||
pkgs.python3Packages.setuptools_scm
|
||||
pkgs.python3Packages.pkgconfig
|
||||
pkgs.python3Packages.cffi
|
||||
pkgs.python3Packages.imap-tools
|
||||
@@ -462,6 +522,18 @@
|
||||
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
cargo-deny
|
||||
perl # needed to build vendored OpenSSL
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
201
fuzz/Cargo.lock
generated
201
fuzz/Cargo.lock
generated
@@ -339,6 +339,12 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.5.3"
|
||||
@@ -515,9 +521,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.4.0"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
|
||||
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -526,9 +532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "2.5.1"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
|
||||
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -953,7 +959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.136.0"
|
||||
version = "1.137.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.1.1",
|
||||
@@ -989,6 +995,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"openssl-src",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
@@ -1785,9 +1792,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
dependencies = [
|
||||
"fastrand 2.0.1",
|
||||
"futures-core",
|
||||
@@ -1912,9 +1919,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.24"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2051,9 +2058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -2062,12 +2069,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.5"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -2077,12 +2096,6 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.2"
|
||||
@@ -2094,39 +2107,58 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.23"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
|
||||
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.7",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.4",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2180,17 +2212,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"image-webp",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2314,12 +2358,6 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
@@ -3480,11 +3518,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.24"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
|
||||
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
@@ -3492,8 +3530,10 @@ dependencies = [
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -3502,7 +3542,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -3515,7 +3555,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3724,7 +3764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 1.0.2",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
@@ -3738,6 +3778,22 @@ dependencies = [
|
||||
"base64 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -4055,9 +4111,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
@@ -4294,22 +4350,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.38"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4442,9 +4498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -4514,6 +4570,28 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
@@ -5169,9 +5247,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -5265,3 +5343,18 @@ dependencies = [
|
||||
"syn 1.0.107",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -30,6 +30,8 @@ module.exports = {
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
@@ -257,6 +259,7 @@ module.exports = {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_REACTED_BY: 177,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
|
||||
@@ -284,6 +287,7 @@ module.exports = {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
|
||||
DC_STR_VOICEMESSAGE: 7,
|
||||
DC_STR_WELCOME_MESSAGE: 71,
|
||||
DC_STR_YOU_REACTED: 176,
|
||||
DC_TEXT1_DRAFT: 1,
|
||||
DC_TEXT1_SELF: 3,
|
||||
DC_TEXT1_USERNAME: 2,
|
||||
|
||||
@@ -37,5 +37,7 @@ module.exports = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE'
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export enum C {
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
@@ -257,6 +259,7 @@ export enum C {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_REACTED_BY = 177,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
@@ -284,6 +287,7 @@ export enum C {
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_STR_YOU_REACTED = 176,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
@@ -331,4 +335,6 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
}
|
||||
|
||||
@@ -3048,14 +3048,6 @@ NAPI_METHOD(dcn_accounts_select_account) {
|
||||
NAPI_RETURN_UINT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_all_work_done) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
|
||||
int result = dc_accounts_all_work_done(dcn_accounts->dc_accounts);
|
||||
NAPI_RETURN_INT32(result);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_io) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
@@ -3382,7 +3374,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_all_work_done);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
|
||||
NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);
|
||||
|
||||
@@ -26,6 +26,8 @@ function createTempUser(chatmailDomain) {
|
||||
}
|
||||
|
||||
describe('static tests', function () {
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
|
||||
it('reverse lookup of events', function () {
|
||||
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
|
||||
const eventValues = Object.values(EventId2EventName)
|
||||
@@ -701,7 +703,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
})
|
||||
|
||||
describe('Integration tests', function () {
|
||||
this.timeout(60 * 3000) // increase timeout to 1min
|
||||
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
|
||||
|
||||
let [dc, context, accountId, directory, account] = [
|
||||
null,
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.136.6"
|
||||
"version": "1.137.2"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.137.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
@@ -26,9 +27,6 @@ dependencies = [
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
@@ -44,11 +42,6 @@ deltachat = [
|
||||
"py.typed"
|
||||
]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
git_describe_command = "git describe --dirty --tags --long --match v*.*"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
|
||||
@@ -547,13 +547,14 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Bob joins the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the verified group.
|
||||
- The "member added" message is not a system message but a plain text message.
|
||||
- Bob's second device doesn't display the Alice's avatar (bug #5354).
|
||||
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
|
||||
missing, cannot encrypt".
|
||||
"""
|
||||
@@ -568,6 +569,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: set avatar")
|
||||
avatar_path = data.get_path("d.png")
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
@@ -580,11 +585,13 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
ac2_offl.start_io()
|
||||
# Receive "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
contact = msg_in.get_sender_contact()
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -399,7 +398,7 @@ def test_enable_mvbox_move(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, sentbox_watch=True)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
|
||||
@@ -407,10 +406,18 @@ def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac2 and ac1: waiting for configuration")
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
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_sentbox_folder") != "Sent":
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
|
||||
def test_move_works(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
@@ -1485,107 +1492,6 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||
|
||||
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||
with online test as follows:
|
||||
- Bob enables download limit and goes offline.
|
||||
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||
- Bob goes online
|
||||
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
ac2.set_config("download_limit", str(download_limit))
|
||||
ac2.stop_io()
|
||||
|
||||
reactions_queue = queue.Queue()
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_reactions_changed(self, message):
|
||||
reactions_queue.put(message)
|
||||
|
||||
ac2.add_account_plugin(InPlugin())
|
||||
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
for m in msgs:
|
||||
ac1._evtracker.wait_msg_delivered(m)
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||
# have a later INTERNALDATE.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
ac1._evtracker.wait_msg_delivered(msgs[-1])
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1_addr
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
|
||||
lp.sec("sending message + reaction from ac1 to ac2")
|
||||
msg1 = chat1.send_text("hi")
|
||||
ac1._evtracker.wait_msg_delivered(msg1)
|
||||
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||
ac2.direct_imap.connect()
|
||||
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
|
||||
ac2.direct_imap.conn.move(uid, "DeltaChat")
|
||||
|
||||
lp.sec("receiving messages by ac2")
|
||||
ac2.start_io()
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.text == msg1.text
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contacts[0].addr == ac1.get_config("addr")
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1, some1) = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -2222,17 +2128,21 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
|
||||
def test_trash_multiple_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.stop_io()
|
||||
|
||||
# Create the Trash folder on IMAP server
|
||||
# and recreate the account so Trash folder is configured.
|
||||
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
|
||||
# Trash wasn't configured initially, it can't be configured later, let's check this.
|
||||
lp.sec("Creating trash folder")
|
||||
ac2.direct_imap.create_folder("Trash")
|
||||
lp.sec("Creating new accounts")
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac2.set_config("delete_to_trash", "1")
|
||||
assert ac2.get_config("configured_trash_folder")
|
||||
|
||||
lp.sec("Check that Trash can be configured initially as well")
|
||||
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
acfactory.bring_accounts_online()
|
||||
assert ac3.get_config("configured_trash_folder")
|
||||
ac3.stop_io()
|
||||
|
||||
ac2.start_io()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending 3 messages")
|
||||
@@ -2247,6 +2157,9 @@ def test_trash_multiple_messages(acfactory, lp):
|
||||
assert msg.text in texts
|
||||
if text != "second":
|
||||
to_delete.append(msg)
|
||||
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
|
||||
# check the configuration.
|
||||
assert ac2.get_config("configured_trash_folder") == "Trash"
|
||||
|
||||
lp.sec("ac2: deleting all messages except second")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
|
||||
@@ -47,7 +47,7 @@ deps =
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
black --quiet --check --diff setup.py src/deltachat examples/ tests/
|
||||
ruff src/deltachat tests/ examples/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-03-19
|
||||
2024-04-05
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.76.0
|
||||
RUST_VERSION=1.77.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -73,6 +73,8 @@ def main():
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
"deltachat-repl/Cargo.toml",
|
||||
"python/pyproject.toml",
|
||||
"deltachat-rpc-client/pyproject.toml",
|
||||
]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
|
||||
@@ -253,25 +253,6 @@ impl Accounts {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
///
|
||||
/// Returns whether all accounts finished their background work.
|
||||
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
||||
///
|
||||
/// iOS can:
|
||||
/// - call dc_start_io() (in case IO was not running)
|
||||
/// - call dc_maybe_network()
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
pub async fn start_io(&mut self) {
|
||||
for account in self.accounts.values_mut() {
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -14,7 +15,6 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::tools::time;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
|
||||
30
src/blob.rs
30
src/blob.rs
@@ -11,9 +11,8 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::{
|
||||
DynamicImage, GenericImage, GenericImageView, ImageFormat, ImageOutputFormat, Pixel, Rgba,
|
||||
};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
@@ -37,6 +36,12 @@ pub struct BlobObject<'a> {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ImageOutputFormat {
|
||||
Png,
|
||||
Jpeg { quality: u8 },
|
||||
}
|
||||
|
||||
impl<'a> BlobObject<'a> {
|
||||
/// Creates a new blob object with a unique name.
|
||||
///
|
||||
@@ -457,9 +462,13 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg(jpeg_quality)
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
}
|
||||
}
|
||||
_ => ImageOutputFormat::Jpeg(jpeg_quality),
|
||||
_ => ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
},
|
||||
};
|
||||
// We need to rewrite images with Exif to remove metadata such as location,
|
||||
// camera model, etc.
|
||||
@@ -530,7 +539,7 @@ impl<'a> BlobObject<'a> {
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
let file_name = blob_abs.file_name().context("No image file name (???)")?;
|
||||
@@ -685,7 +694,13 @@ fn encode_img(
|
||||
) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
let mut buf = Cursor::new(encoded);
|
||||
img.write_to(&mut buf, fmt)?;
|
||||
match fmt {
|
||||
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
|
||||
ImageOutputFormat::Jpeg { quality } => {
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
|
||||
img.write_with_encoder(encoder)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -725,7 +740,6 @@ fn add_white_bg(img: &mut DynamicImage) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::File;
|
||||
use image::Pixel;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
|
||||
240
src/chat.rs
240
src/chat.rs
@@ -8,6 +8,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -15,13 +16,14 @@ use strum_macros::EnumIter;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::chatlist_events;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -43,7 +45,7 @@ use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty, SystemTime,
|
||||
smeared_time, time, IsNoneOrEmpty, SystemTime,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
|
||||
@@ -209,24 +211,11 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Returns [`ChatId`] of a chat that `msg` belongs to.
|
||||
///
|
||||
/// Checks that `msg` is assigned to the right chat.
|
||||
pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
|
||||
if msg.chat_id == DC_CHAT_ID_TRASH {
|
||||
return None;
|
||||
}
|
||||
if msg.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| msg
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If `msg` is not fully downloaded or undecipherable, it may have been assigned to the
|
||||
// wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
if msg.download_state == DownloadState::Undecipherable {
|
||||
return None;
|
||||
}
|
||||
Some(msg.chat_id)
|
||||
@@ -308,6 +297,8 @@ impl ChatId {
|
||||
}
|
||||
};
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -424,6 +415,7 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||
@@ -446,6 +438,8 @@ impl ChatId {
|
||||
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
self.set_blocked(context, Blocked::Not).await?;
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||
@@ -456,6 +450,7 @@ impl ChatId {
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -499,6 +494,7 @@ impl ChatId {
|
||||
|
||||
if self.set_blocked(context, Blocked::Not).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
@@ -541,6 +537,7 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
@@ -589,6 +586,7 @@ impl ChatId {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -675,6 +673,8 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
@@ -781,6 +781,7 @@ impl ChatId {
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
@@ -792,6 +793,7 @@ impl ChatId {
|
||||
msg.text = stock_str::self_deleted_msg_body(context).await;
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1172,6 +1174,15 @@ impl ChatId {
|
||||
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
/// Returns chat member list timestamp.
|
||||
pub(crate) async fn get_member_list_timestamp(self, context: &Context) -> Result<i64> {
|
||||
Ok(self
|
||||
.get_param(context)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn parent_query<T, F>(
|
||||
self,
|
||||
context: &Context,
|
||||
@@ -1430,7 +1441,7 @@ impl rusqlite::types::ToSql for ChatId {
|
||||
impl rusqlite::types::FromSql for ChatId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
if 0 <= val && val <= i64::from(u32::MAX) {
|
||||
Ok(ChatId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -1518,7 +1529,7 @@ impl Chat {
|
||||
Ok(contacts) => {
|
||||
if let Some(contact_id) = contacts.first() {
|
||||
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
|
||||
chat_name = contact.get_display_name().to_owned();
|
||||
contact.get_display_name().clone_into(&mut chat_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1789,12 +1800,10 @@ impl Chat {
|
||||
update_msg_id: Option<MsgId>,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
let mut new_references = "".into();
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let new_rfc724_mid = create_outgoing_rfc724_mid(&from);
|
||||
let new_rfc724_mid = create_outgoing_rfc724_mid();
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
if let Some(id) = context
|
||||
@@ -1832,58 +1841,72 @@ impl Chat {
|
||||
// reset encrypt error state eg. for forwarding
|
||||
msg.param.remove(Param::ErroneousE2ee);
|
||||
|
||||
// set "In-Reply-To:" to identify the message to which the composed message is a reply;
|
||||
// set "References:" to identify the "thread" of the conversation;
|
||||
// both according to RFC 5322 3.6.4, page 25
|
||||
//
|
||||
// as self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
if !self.is_self_talk() {
|
||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
// the whole list of messages referenced may be huge;
|
||||
// only use the oldest and the parent message
|
||||
let parent_references = parent_references
|
||||
.find(' ')
|
||||
.and_then(|n| parent_references.get(..n))
|
||||
.unwrap_or(&parent_references);
|
||||
|
||||
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
// angle brackets are added by the mimefactory later
|
||||
new_references = format!("{parent_references} {parent_rfc724_mid}");
|
||||
} else if !parent_references.is_empty() {
|
||||
new_references = parent_references.to_string();
|
||||
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
|
||||
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
|
||||
} else if !parent_in_reply_to.is_empty() {
|
||||
new_references = parent_in_reply_to;
|
||||
} else {
|
||||
// as a fallback, use our Message-ID, see reasoning below.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
} else {
|
||||
// this is a top-level message, add our Message-ID as first reference.
|
||||
// as we always try to extract the grpid also from `References:`-header,
|
||||
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
|
||||
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
|
||||
new_references = new_rfc724_mid.clone();
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
let new_references;
|
||||
if self.is_self_talk() {
|
||||
// As self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
new_references = String::new();
|
||||
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
// affected by those messages, e.g. they could add new members to a group and the
|
||||
// new message will contain them in "To:". Anyway recipients must be prepared to
|
||||
// orphaned references.
|
||||
self
|
||||
.id
|
||||
.get_parent_mime_headers(context, MessageState::OutPending)
|
||||
.await?
|
||||
{
|
||||
// "In-Reply-To:" is not changed if it is set manually.
|
||||
// This does not affect "References:" header, it will contain "default parent" (the
|
||||
// latest message in the thread) anyway.
|
||||
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
|
||||
msg.in_reply_to = Some(parent_rfc724_mid.clone());
|
||||
}
|
||||
|
||||
// Use parent `In-Reply-To` as a fallback
|
||||
// in case parent message has no `References` header
|
||||
// as specified in RFC 5322:
|
||||
// > If the parent message does not contain
|
||||
// > a "References:" field but does have an "In-Reply-To:" field
|
||||
// > containing a single message identifier, then the "References:" field
|
||||
// > will contain the contents of the parent's "In-Reply-To:" field
|
||||
// > followed by the contents of the parent's "Message-ID:" field (if
|
||||
// > any).
|
||||
let parent_references = if parent_references.is_empty() {
|
||||
parent_in_reply_to
|
||||
} else {
|
||||
parent_references
|
||||
};
|
||||
|
||||
// The whole list of messages referenced may be huge.
|
||||
// Only take 2 recent references and add third from `In-Reply-To`.
|
||||
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
|
||||
references_vec.reverse();
|
||||
|
||||
if !parent_rfc724_mid.is_empty()
|
||||
&& !references_vec.contains(&parent_rfc724_mid.as_str())
|
||||
{
|
||||
references_vec.push(&parent_rfc724_mid)
|
||||
}
|
||||
|
||||
if references_vec.is_empty() {
|
||||
// As a fallback, use our Message-ID,
|
||||
// same as in the case of top-level message.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
} else {
|
||||
new_references = references_vec.join(" ");
|
||||
}
|
||||
} else {
|
||||
// This is a top-level message.
|
||||
// Add our Message-ID as first references.
|
||||
// This allows us to identify replies to our message even if
|
||||
// email server such as Outlook changes `Message-ID:` header.
|
||||
// MUAs usually keep the first Message-ID in `References:` header unchanged.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
}
|
||||
|
||||
// add independent location to database
|
||||
@@ -2686,7 +2709,9 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
@@ -2730,17 +2755,8 @@ async fn prepare_send_msg(
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, msg).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
@@ -2799,15 +2815,21 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, now).await?;
|
||||
}
|
||||
|
||||
if rendered_msg.is_group {
|
||||
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
// Reject member list synchronisation from older messages. See also
|
||||
// `receive_imf::apply_group_changes()`.
|
||||
msg.chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, now)
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
now.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -2841,7 +2863,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.update_subject(context).await?;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
@@ -3096,7 +3118,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.await?;
|
||||
for chat_id_in_archive in chat_ids_in_archive {
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
|
||||
}
|
||||
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
|
||||
} else {
|
||||
let exists = context
|
||||
.sql
|
||||
@@ -3123,6 +3147,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3190,6 +3215,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
|
||||
for c in changed_chats {
|
||||
context.emit_event(EventType::MsgsNoticed(c));
|
||||
chatlist_events::emit_chatlist_item_changed(context, c);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3352,6 +3378,8 @@ pub async fn create_group_chat(
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
@@ -3439,11 +3467,14 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
if sync.into() {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
let action = SyncAction::CreateBroadcast(chat_name);
|
||||
self::sync(context, id, action).await.log_err(context).ok();
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
@@ -3714,6 +3745,7 @@ pub(crate) async fn set_muted_ex(
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
if sync.into() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.sync(context, SyncAction::SetMuted(duration))
|
||||
@@ -3874,6 +3906,7 @@ async fn rename_ex(
|
||||
sync = Nosync;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -3934,6 +3967,7 @@ pub async fn set_chat_profile_image(
|
||||
context.emit_msgs_changed(chat_id, msg.id);
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4080,6 +4114,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
msg_id: msg.id,
|
||||
});
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
@@ -4150,7 +4186,7 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(msg) = msg {
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid("@device");
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
@@ -4290,7 +4326,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid("@device");
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
let mut param = Params::new();
|
||||
@@ -4772,9 +4808,9 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test simultaneous removal of user from the chat and leaving the group.
|
||||
/// Test parallel removal of user from the chat and leaving the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_simultaneous_member_remove() -> Result<()> {
|
||||
async fn test_parallel_member_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
@@ -4805,20 +4841,25 @@ mod tests {
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
// This adds Bob because they left quite long ago.
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
|
||||
bob.recv_msg(&alice_sent_msg).await;
|
||||
|
||||
// Test that add message is rewritten.
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
|
||||
.await;
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let alice_sent_remove_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice removing him from the group.
|
||||
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
|
||||
|
||||
@@ -4855,8 +4896,13 @@ mod tests {
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
// This doesn't add Fiona back because Bob just removed them.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
|
||||
.await;
|
||||
Ok(())
|
||||
|
||||
@@ -416,7 +416,7 @@ impl Chatlist {
|
||||
if chat.id.is_archived_link() {
|
||||
Ok(Default::default())
|
||||
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
|
||||
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
|
||||
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
|
||||
} else {
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
@@ -13,7 +14,6 @@ use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::{self, DC_VERSION_STR};
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
@@ -574,6 +574,9 @@ impl Context {
|
||||
_ => Default::default(),
|
||||
};
|
||||
self.set_config_internal(key, value).await?;
|
||||
if key == Config::SentboxWatch {
|
||||
self.last_full_folder_scan.lock().await.take();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
@@ -23,7 +24,6 @@ use server_params::{expand_param_vector, ServerParams};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::imap::{session::Session as ImapSession, Imap};
|
||||
use crate::log::LogExt;
|
||||
@@ -35,8 +35,9 @@ use crate::smtp::Smtp;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
use crate::tools::time;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
@@ -356,8 +357,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut smtp_configured = false;
|
||||
let mut errors = Vec::new();
|
||||
for smtp_server in smtp_servers {
|
||||
smtp_param.user = smtp_server.username.clone();
|
||||
smtp_param.server = smtp_server.hostname.clone();
|
||||
smtp_param.user.clone_from(&smtp_server.username);
|
||||
smtp_param.server.clone_from(&smtp_server.hostname);
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
smtp_param.certificate_checks = match smtp_server.strict_tls {
|
||||
@@ -403,8 +404,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let imap_servers_count = imap_servers.len();
|
||||
let mut errors = Vec::new();
|
||||
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
|
||||
param.imap.user = imap_server.username.clone();
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.user.clone_from(&imap_server.username);
|
||||
param.imap.server.clone_from(&imap_server.hostname);
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
param.imap.certificate_checks = match imap_server.strict_tls {
|
||||
|
||||
@@ -219,6 +219,10 @@ pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
|
||||
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
|
||||
|
||||
/// How far in the future the sender timestamp of a message is allowed to be, in seconds. Also used
|
||||
/// in the group membership consistency algo to reject outdated membership changes.
|
||||
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
306
src/contact.rs
306
src/contact.rs
@@ -3,15 +3,17 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
pub use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{
|
||||
addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, strip_rtlo_characters,
|
||||
ContactAddress,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
@@ -33,60 +35,12 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress, SystemTime,
|
||||
};
|
||||
use crate::{chat, stock_str};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContactAddress(String);
|
||||
|
||||
impl Deref for ContactAddress {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContactAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactAddress {
|
||||
/// Constructs a new contact address from string,
|
||||
/// normalizing and validating it.
|
||||
pub fn new(s: &str) -> Result<Self> {
|
||||
let addr = addr_normalize(s);
|
||||
if !may_be_valid_addr(&addr) {
|
||||
bail!("invalid address {:?}", s);
|
||||
}
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -760,6 +714,7 @@ impl Contact {
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,7 +751,9 @@ impl Contact {
|
||||
Ok(row_id)
|
||||
}).await?;
|
||||
|
||||
Ok((ContactId::new(row_id), sth_modified))
|
||||
let contact_id = ContactId::new(row_id);
|
||||
|
||||
Ok((contact_id, sth_modified))
|
||||
}
|
||||
|
||||
/// Add a number of contacts.
|
||||
@@ -1051,55 +1008,52 @@ impl Contact {
|
||||
"Can not provide encryption info for special contact"
|
||||
);
|
||||
|
||||
let mut ret = String::new();
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
if let Some(peerstate) =
|
||||
peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
{
|
||||
let stock_message = match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
|
||||
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
else {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
ret += &format!("{stock_message}.\n{finger_prints}:");
|
||||
let stock_message = match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
|
||||
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if loginparam.addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
}
|
||||
} else {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
}
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
let mut ret = format!("{stock_message}.\n{finger_prints}:");
|
||||
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(true)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(false)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if loginparam.addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
@@ -1415,46 +1369,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
/// Returns address lowercased,
|
||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||
pub fn addr_normalize(addr: &str) -> String {
|
||||
let norm = addr.trim().to_lowercase();
|
||||
|
||||
if norm.starts_with("mailto:") {
|
||||
norm.get(7..).unwrap_or(&norm).to_string()
|
||||
} else {
|
||||
norm
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
@@ -1527,6 +1441,7 @@ WHERE type=? AND id IN (
|
||||
}
|
||||
}
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1577,6 +1492,7 @@ pub(crate) async fn set_profile_image(
|
||||
if changed {
|
||||
contact.update_param(context).await?;
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1632,6 +1548,7 @@ pub(crate) async fn update_last_seen(
|
||||
> 0
|
||||
&& timestamp > time() - SEEN_RECENTLY_SECONDS
|
||||
{
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_recently_seen(contact_id, timestamp)
|
||||
@@ -1640,26 +1557,6 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_fingerprint(
|
||||
ret: &mut String,
|
||||
addr: &str,
|
||||
@@ -1683,14 +1580,6 @@ fn cat_fingerprint(
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two email addresses, normalizing them beforehand.
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1);
|
||||
let norm2 = addr_normalize(addr2);
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
@@ -1762,6 +1651,7 @@ impl RecentlySeenLoop {
|
||||
.unwrap_or_default();
|
||||
|
||||
loop {
|
||||
let now = SystemTime::now();
|
||||
let (until, contact_id) =
|
||||
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
|
||||
(
|
||||
@@ -1787,6 +1677,11 @@ impl RecentlySeenLoop {
|
||||
// Timeout, notify about contact.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
|
||||
&context,
|
||||
*contact_id,
|
||||
)
|
||||
.await;
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
@@ -1804,7 +1699,10 @@ impl RecentlySeenLoop {
|
||||
timestamp,
|
||||
})) => {
|
||||
// Received an interrupt.
|
||||
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
|
||||
if contact_id != ContactId::UNDEFINED {
|
||||
unseen_queue
|
||||
.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1816,13 +1714,18 @@ impl RecentlySeenLoop {
|
||||
// Event is already in the past.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
|
||||
&context,
|
||||
*contact_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
pub(crate) fn try_interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.interrupt_send
|
||||
.try_send(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
@@ -1831,6 +1734,17 @@ impl RecentlySeenLoop {
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.interrupt_send
|
||||
.send(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
timestamp,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn abort(self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
@@ -1838,6 +1752,8 @@ impl RecentlySeenLoop {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
@@ -1977,18 +1893,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
let contact_address = ContactAddress::new(alice_addr)?;
|
||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||
|
||||
let invalid_addr = "<> foobar";
|
||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_or_lookup() {
|
||||
// add some contacts, this also tests add_address_book()
|
||||
@@ -2812,6 +2716,44 @@ Hi."#;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently_event() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let recently_seen_loop = RecentlySeenLoop::new(bob.ctx.clone());
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
|
||||
for _ in 0..2 {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
while bob.evtracker.try_recv().is_ok() {}
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
recently_seen_loop
|
||||
.interrupt(contact.id, contact.last_seen)
|
||||
.await;
|
||||
|
||||
// Wait for `was_seen_recently()` to turn off.
|
||||
while bob.evtracker.try_recv().is_ok() {}
|
||||
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
|
||||
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -16,6 +16,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
|
||||
@@ -593,11 +594,15 @@ impl Context {
|
||||
/// Emits a MsgsChanged event with specified chat and message ids
|
||||
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
|
||||
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
chatlist_events::emit_chatlist_item_changed(self, chat_id);
|
||||
}
|
||||
|
||||
/// Emits an IncomingMsg event with specified chat and message ids
|
||||
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
|
||||
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
chatlist_events::emit_chatlist_item_changed(self, chat_id);
|
||||
}
|
||||
|
||||
/// Returns a receiver for emitted events.
|
||||
@@ -1395,7 +1400,7 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid(contact.get_addr())
|
||||
create_outgoing_rfc724_mid()
|
||||
);
|
||||
println!("{msg}");
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
|
||||
@@ -64,6 +64,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
|
||||
document: None,
|
||||
uid: None,
|
||||
},
|
||||
time,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -4,12 +4,12 @@ use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::authres::{self, DkimResults};
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
|
||||
@@ -301,7 +301,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
let href = href
|
||||
.decode_and_unescape_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
.to_string();
|
||||
|
||||
if !href.is_empty() {
|
||||
dehtml.last_href = Some(href);
|
||||
@@ -463,6 +463,13 @@ mod tests {
|
||||
assert_eq!(plain, "[text](url)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_case_sensitive_link() {
|
||||
let html = "<html><A HrEf=\"https://foo.bar/Data\">case in URLs matter</A></html>";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
assert_eq!(plain, "[case in URLs matter](https://foo.bar/Data)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_bold_text() {
|
||||
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::imap::session::Session;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::tools::time;
|
||||
use crate::{stock_str, EventType};
|
||||
use crate::{chatlist_events, stock_str, EventType};
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
@@ -115,6 +115,7 @@ impl MsgId {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: self,
|
||||
});
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -448,10 +449,9 @@ mod tests {
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert!(Message::load_from_db(&bob, msg.id)
|
||||
assert!(Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
.chat_id
|
||||
.is_trash());
|
||||
.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -507,10 +507,9 @@ mod tests {
|
||||
// (usually mdn are too small for not being downloaded directly)
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert!(Message::load_from_db(&bob, msg.id)
|
||||
assert!(Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
.chat_id
|
||||
.is_trash());
|
||||
.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -392,7 +392,8 @@ WHERE
|
||||
SELECT id, chat_id, type
|
||||
FROM msgs
|
||||
WHERE
|
||||
timestamp < ?
|
||||
timestamp < ?1
|
||||
AND timestamp_rcvd < ?1
|
||||
AND chat_id > ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?
|
||||
@@ -490,7 +491,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
|
||||
.sql
|
||||
.query_get_value(
|
||||
r#"
|
||||
SELECT min(timestamp)
|
||||
SELECT min(max(timestamp, timestamp_rcvd))
|
||||
FROM msgs
|
||||
WHERE chat_id > ?
|
||||
AND chat_id != ?
|
||||
@@ -1097,8 +1098,8 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
|
||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||
assert!(loaded.is_none());
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_is_deleted(t, chat, msg_id).await;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use async_channel::{self as channel, Receiver, Sender, TrySendError};
|
||||
use pin_project::pin_project;
|
||||
|
||||
pub(crate) mod chatlist_events;
|
||||
mod payload;
|
||||
|
||||
pub use self::payload::EventType;
|
||||
|
||||
635
src/events/chatlist_events.rs
Normal file
635
src/events/chatlist_events.rs
Normal file
@@ -0,0 +1,635 @@
|
||||
use crate::{chat::ChatId, contact::ContactId, context::Context, EventType};
|
||||
|
||||
/// order or content of chatlist changes (chat ids, not the actual chatlist item)
|
||||
pub(crate) fn emit_chatlist_changed(context: &Context) {
|
||||
context.emit_event(EventType::ChatlistChanged);
|
||||
}
|
||||
|
||||
/// Chatlist item of a specific chat changed
|
||||
pub(crate) fn emit_chatlist_item_changed(context: &Context, chat_id: ChatId) {
|
||||
context.emit_event(EventType::ChatlistItemChanged {
|
||||
chat_id: Some(chat_id),
|
||||
});
|
||||
}
|
||||
|
||||
/// Used when you don't know which chatlist items changed, this reloads all cached chatlist items in the UI
|
||||
///
|
||||
/// Avoid calling this when you can find out the affected chat ids easialy (without extra expensive db queries).
|
||||
///
|
||||
/// This method is not public, so you have to define and document your new case here in this file.
|
||||
fn emit_unknown_chatlist_items_changed(context: &Context) {
|
||||
context.emit_event(EventType::ChatlistItemChanged { chat_id: None });
|
||||
}
|
||||
|
||||
/// update event for the 1:1 chat with the contact
|
||||
/// used when recently seen changes and when profile image changes
|
||||
pub(crate) async fn emit_chatlist_item_changed_for_contact_chat(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) {
|
||||
match ChatId::lookup_by_contact(context, contact_id).await {
|
||||
Ok(Some(chat_id)) => self::emit_chatlist_item_changed(context, chat_id),
|
||||
Ok(None) => {}
|
||||
Err(error) => context.emit_event(EventType::Error(format!(
|
||||
"failed to find chat id for contact for chatlist event: {error:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// update items for chats that have the contact
|
||||
/// used when contact changes their name or did AEAP for example
|
||||
///
|
||||
/// The most common case is that the contact changed their name
|
||||
/// and their name should be updated in the chatlistitems for the chats
|
||||
/// where they sent the last message as there their name is shown in the summary on those
|
||||
pub(crate) fn emit_chatlist_items_changed_for_contact(context: &Context, _contact_id: ContactId) {
|
||||
// note:(treefit): it is too expensive to find the right chats
|
||||
// so we'll just tell ui to reload every loaded item
|
||||
emit_unknown_chatlist_items_changed(context)
|
||||
// note:(treefit): in the future we could instead emit an extra event for this and also store contact id in the chatlistitems
|
||||
// (contact id for dm chats and contact id of contact that wrote the message in the summary)
|
||||
// the ui could then look for this info in the cache and only reload the needed chats.
|
||||
}
|
||||
|
||||
/// Tests for chatlist events
|
||||
///
|
||||
/// Only checks if the events are emitted,
|
||||
/// does not check for excess/too-many events
|
||||
#[cfg(test)]
|
||||
mod test_chatlist_events {
|
||||
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
chat::{
|
||||
self, create_broadcast_list, create_group_chat, set_muted, ChatId, ChatVisibility,
|
||||
MuteDuration, ProtectionStatus,
|
||||
},
|
||||
config::Config,
|
||||
constants::*,
|
||||
contact::Contact,
|
||||
message::{self, Message, MessageState},
|
||||
reaction,
|
||||
receive_imf::receive_imf,
|
||||
securejoin::{get_securejoin_qr, join_securejoin},
|
||||
test_utils::{TestContext, TestContextManager},
|
||||
EventType,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
async fn wait_for_chatlist_and_specific_item(context: &TestContext, chat_id: ChatId) {
|
||||
let first_event_is_item = AtomicBool::new(false);
|
||||
context
|
||||
.evtracker
|
||||
.get_matching(|evt| match evt {
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(ev_chat_id),
|
||||
} => {
|
||||
if ev_chat_id == &chat_id {
|
||||
first_event_is_item.store(true, Ordering::Relaxed);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
EventType::ChatlistChanged => true,
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
if first_event_is_item.load(Ordering::Relaxed) {
|
||||
wait_for_chatlist(context).await;
|
||||
} else {
|
||||
wait_for_chatlist_specific_item(context, chat_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_chatlist_specific_item(context: &TestContext, chat_id: ChatId) {
|
||||
context
|
||||
.evtracker
|
||||
.get_matching(|evt| match evt {
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(ev_chat_id),
|
||||
} => ev_chat_id == &chat_id,
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_chatlist_all_items(context: &TestContext) {
|
||||
context
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatlistItemChanged { chat_id: None }))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_chatlist(context: &TestContext) {
|
||||
context
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatlistChanged))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_chat_visibility() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat_id = create_group_chat(
|
||||
&alice,
|
||||
crate::chat::ProtectionStatus::Unprotected,
|
||||
"my_group",
|
||||
)
|
||||
.await?;
|
||||
|
||||
chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Pinned)
|
||||
.await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
|
||||
|
||||
chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Archived)
|
||||
.await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
|
||||
|
||||
chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Normal)
|
||||
.await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// mute a chat, archive it, then use another account to send a message to it, the counter on the archived chatlist item should change
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_counter_increases_for_muted_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
bob_chat
|
||||
.id
|
||||
.set_visibility(&bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
set_muted(&bob, bob_chat.id, MuteDuration::Forever).await?;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
|
||||
let sent_msg = alice.send_text(chat.id, "moin2").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
bob.evtracker
|
||||
.get_matching(|evt| match evt {
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(chat_id),
|
||||
} => chat_id.is_archived_link(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark noticed on archive-link chatlistitem should update the unread counter on it
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_counter_update_on_mark_noticed() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
bob_chat
|
||||
.id
|
||||
.set_visibility(&bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
set_muted(&bob, bob_chat.id, MuteDuration::Forever).await?;
|
||||
let sent_msg = alice.send_text(chat.id, "moin2").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
chat::marknoticed_chat(&bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
wait_for_chatlist_specific_item(&bob, DC_CHAT_ID_ARCHIVED_LINK).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Contact name update - expect all chats to update
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_name_update() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice_to_bob_chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(alice_to_bob_chat.id, "hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
// set alice name then receive messagefrom her with bob
|
||||
alice.set_config(Config::Displayname, Some("Alice")).await?;
|
||||
let sent_msg = alice
|
||||
.send_text(alice_to_bob_chat.id, "hello, I set a displayname")
|
||||
.await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let alice_on_bob = bob.add_or_lookup_contact(&alice).await;
|
||||
assert!(alice_on_bob.get_display_name() == "Alice");
|
||||
|
||||
wait_for_chatlist_all_items(&bob).await;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
// set name
|
||||
let addr = alice_on_bob.get_addr();
|
||||
Contact::create(&bob, "Alice2", addr).await?;
|
||||
assert!(bob.add_or_lookup_contact(&alice).await.get_display_name() == "Alice2");
|
||||
|
||||
wait_for_chatlist_all_items(&bob).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Contact changed avatar
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_changed_avatar() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice_to_bob_chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(alice_to_bob_chat.id, "hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
// set alice avatar then receive messagefrom her with bob
|
||||
let file = alice.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
alice
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
let sent_msg = alice
|
||||
.send_text(alice_to_bob_chat.id, "hello, I have a new avatar")
|
||||
.await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let alice_on_bob = bob.add_or_lookup_contact(&alice).await;
|
||||
assert!(alice_on_bob.get_profile_image(&bob).await?.is_some());
|
||||
|
||||
wait_for_chatlist_specific_item(&bob, bob.create_chat(&alice).await.id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete chat
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat.delete(&alice).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create group chat
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
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_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create broadcastlist
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_broadcastlist() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
create_broadcast_list(&alice).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mute chat
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mute_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::set_muted(&alice, chat, MuteDuration::NotMuted).await?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Expiry of mute should also trigger an event
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "does not work yet"]
|
||||
async fn test_mute_chat_expired() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
|
||||
let mute_duration = MuteDuration::Until(
|
||||
std::time::SystemTime::now()
|
||||
.checked_add(Duration::from_secs(2))
|
||||
.unwrap(),
|
||||
);
|
||||
chat::set_muted(&alice, chat, mute_duration).await?;
|
||||
alice.evtracker.clear_events();
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change chat name
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_chat_name() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().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?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change chat profile image
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_chat_profile_image() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
let file = alice.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
chat::set_chat_profile_image(&alice, chat, file.to_str().unwrap()).await?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive group and receive name change
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receiving_group_and_group_changes() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.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;
|
||||
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
|
||||
chat_id_for_bob.accept(&bob).await?;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
chat::set_chat_name(&alice, chat, "New Name").await?;
|
||||
let sent_msg = alice.send_text(chat, "Hello").await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept contact request
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_contact_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.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;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
chat_id_for_bob.accept(&bob).await?;
|
||||
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Block contact request
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_block_contact_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.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;
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
chat_id_for_bob.block(&bob).await?;
|
||||
wait_for_chatlist(&bob).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete message
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().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();
|
||||
message::delete_msgs(&alice, &[message]).await?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Click on chat should remove the unread count (on msgs noticed)
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msgs_noticed_on_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.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?;
|
||||
|
||||
let sent_msg = alice.send_text(chat, "New Message").await;
|
||||
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
|
||||
assert!(chat_id_for_bob.get_fresh_msg_cnt(&bob).await? >= 1);
|
||||
|
||||
bob.evtracker.clear_events();
|
||||
chat::marknoticed_chat(&bob, chat_id_for_bob).await?;
|
||||
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Block and Unblock contact
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unblock_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let contact_id = Contact::create(&alice, "example", "example@example.com").await?;
|
||||
let _ = ChatId::create_for_contact(&alice, contact_id).await;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
Contact::block(&alice, contact_id).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
Contact::unblock(&alice, contact_id).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ephemeral / disappearing messages
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_after_ephemeral_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
|
||||
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 1 })
|
||||
.await?;
|
||||
let _ = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
wait_for_chatlist_and_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// AdHoc (Groups without a group ID.) group receiving
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let mime = br#"Subject: First thread
|
||||
Message-ID: first@example.org
|
||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First thread."#;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
receive_imf(&alice, mime, false).await?;
|
||||
wait_for_chatlist(&alice).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test both direction of securejoin
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_secure_join_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().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?;
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vg-request
|
||||
bob.evtracker.clear_events();
|
||||
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
||||
wait_for_chatlist(&bob).await;
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
alice.evtracker.clear_events();
|
||||
alice.recv_msg(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent).await;
|
||||
wait_for_chatlist_and_specific_item(&bob, bob_chatid).await;
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
||||
alice.evtracker.clear_events();
|
||||
alice.recv_msg(&sent).await;
|
||||
wait_for_chatlist_and_specific_item(&alice, alice_chatid).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Step 7: Bob receives vg-member-added
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent).await;
|
||||
wait_for_chatlist_and_specific_item(&bob, bob_chatid).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call Resend on message
|
||||
///
|
||||
/// (the event is technically only needed if it is the last message in the chat, but checking that would be too expensive so the event is always emitted)
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().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;
|
||||
|
||||
let message = Message::load_from_db(&alice, msg_id).await?;
|
||||
assert_eq!(message.get_state(), MessageState::OutDelivered);
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
chat::resend_msgs(&alice, &[msg_id]).await?;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// test that setting a reaction emits chatlistitem update event
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().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;
|
||||
|
||||
alice.evtracker.clear_events();
|
||||
reaction::send_reaction(&alice, msg_id, "👍").await?;
|
||||
let _ = alice.pop_sent_msg().await;
|
||||
wait_for_chatlist_specific_item(&alice, chat).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -107,10 +107,7 @@ pub enum EventType {
|
||||
},
|
||||
|
||||
/// Downloading a bunch of messages just finished.
|
||||
IncomingMsgBunch {
|
||||
/// List of incoming message IDs.
|
||||
msg_ids: Vec<MsgId>,
|
||||
},
|
||||
IncomingMsgBunch,
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
@@ -185,7 +182,7 @@ pub enum EventType {
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
/// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
ContactsChanged(Option<ContactId>),
|
||||
@@ -294,4 +291,15 @@ pub enum EventType {
|
||||
///
|
||||
/// This event is only emitted by the account manager
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
///
|
||||
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||
ChatlistChanged,
|
||||
|
||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
ChatlistItemChanged {
|
||||
/// ID of the changed chat
|
||||
chat_id: Option<ChatId>,
|
||||
},
|
||||
}
|
||||
|
||||
166
src/html.rs
166
src/html.rs
@@ -7,12 +7,8 @@
|
||||
//! `MsgId.get_html()` will return HTML -
|
||||
//! this allows nice quoting, handling linebreaks properly etc.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::future::FutureExt;
|
||||
use lettre_email::mime::Mime;
|
||||
use lettre_email::PartBuilder;
|
||||
use mailparse::ParsedContentType;
|
||||
@@ -116,119 +112,109 @@ impl HtmlMsgParser {
|
||||
/// Usually, there is at most one plain-text and one HTML-text part,
|
||||
/// multiple plain-text parts might be used for mailinglist-footers,
|
||||
/// therefore we use the first one.
|
||||
fn collect_texts_recursive<'a>(
|
||||
async fn collect_texts_recursive<'a>(
|
||||
&'a mut self,
|
||||
mail: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in &mail.subparts {
|
||||
self.collect_texts_recursive(cur_data).await?
|
||||
}
|
||||
Ok(())
|
||||
) -> Result<()> {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in &mail.subparts {
|
||||
Box::pin(self.collect_texts_recursive(cur_data)).await?
|
||||
}
|
||||
MimeMultipartType::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
self.collect_texts_recursive(&mail).await
|
||||
Ok(())
|
||||
}
|
||||
MimeMultipartType::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype == mime::TEXT_HTML {
|
||||
if self.html.is_empty() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.html = decoded_data;
|
||||
}
|
||||
}
|
||||
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
Box::pin(self.collect_texts_recursive(&mail)).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype == mime::TEXT_HTML {
|
||||
if self.html.is_empty() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().to_ascii_lowercase() == "flowed"
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().to_ascii_lowercase() == "yes"
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
self.html = decoded_data;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
|
||||
if let Ok(decoded_data) = mail.get_body() {
|
||||
self.plain = Some(PlainText {
|
||||
text: decoded_data,
|
||||
flowed: if let Some(format) = mail.ctype.params.get("format") {
|
||||
format.as_str().to_ascii_lowercase() == "flowed"
|
||||
} else {
|
||||
false
|
||||
},
|
||||
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().to_ascii_lowercase() == "yes"
|
||||
} else {
|
||||
false
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
/// Replace cid:-protocol by the data:-protocol where appropriate.
|
||||
/// This allows the final html-file to be self-contained.
|
||||
fn cid_to_data_recursive<'a>(
|
||||
async fn cid_to_data_recursive<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
mail: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in &mail.subparts {
|
||||
self.cid_to_data_recursive(context, cur_data).await?;
|
||||
}
|
||||
Ok(())
|
||||
) -> Result<()> {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in &mail.subparts {
|
||||
Box::pin(self.cid_to_data_recursive(context, cur_data)).await?;
|
||||
}
|
||||
MimeMultipartType::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
self.cid_to_data_recursive(context, &mail).await
|
||||
Ok(())
|
||||
}
|
||||
MimeMultipartType::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
|
||||
if let Ok(cid) = parse_message_id(&cid) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail) {
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}",
|
||||
re_string,
|
||||
e
|
||||
),
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
Box::pin(self.cid_to_data_recursive(context, &mail)).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
|
||||
if let Ok(cid) = parse_message_id(&cid) {
|
||||
if let Ok(replacement) = mimepart_to_data_url(mail) {
|
||||
let re_string = format!(
|
||||
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
|
||||
regex::escape(&cid)
|
||||
);
|
||||
match regex::Regex::new(&re_string) {
|
||||
Ok(re) => {
|
||||
self.html = re
|
||||
.replace_all(
|
||||
&self.html,
|
||||
format!("${{1}}{replacement}${{3}}").as_str(),
|
||||
)
|
||||
.as_ref()
|
||||
.to_string()
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"Cannot create regex for cid: {} throws {}", re_string, e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/imap.rs
17
src/imap.rs
@@ -15,6 +15,7 @@ use std::{
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::{normalize_name, ContactAddress};
|
||||
use futures::{FutureExt as _, StreamExt, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -22,9 +23,10 @@ use ratelimit::Ratelimit;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, ShowEmails};
|
||||
use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin};
|
||||
use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -388,7 +390,7 @@ impl Imap {
|
||||
Ok(session) => {
|
||||
// Store server ID in the context to display in account info.
|
||||
let mut lock = context.server_id.write().await;
|
||||
*lock = session.capabilities.server_id.clone();
|
||||
lock.clone_from(&session.capabilities.server_id);
|
||||
|
||||
self.login_failed_once = false;
|
||||
context.emit_event(EventType::ImapConnected(format!(
|
||||
@@ -420,7 +422,7 @@ impl Imap {
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = message.clone();
|
||||
msg.text.clone_from(&message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
@@ -717,12 +719,8 @@ impl Imap {
|
||||
|
||||
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
|
||||
|
||||
let msg_ids: Vec<MsgId> = received_msgs
|
||||
.iter()
|
||||
.flat_map(|m| m.msg_ids.clone())
|
||||
.collect();
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
|
||||
if !received_msgs.is_empty() {
|
||||
context.emit_event(EventType::IncomingMsgBunch);
|
||||
}
|
||||
|
||||
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
|
||||
@@ -1174,6 +1172,7 @@ impl Session {
|
||||
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -89,9 +89,16 @@ impl Imap {
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Config::ConfiguredTrashFolder,
|
||||
] {
|
||||
context
|
||||
.set_config_internal(conf, folder_configs.get(&conf).map(|s| s.as_str()))
|
||||
.await?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
last_scan.replace(tools::Time::now());
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use ::pgp::types::KeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use futures::StreamExt;
|
||||
use futures_lite::FutureExt;
|
||||
use rand::{thread_rng, Rng};
|
||||
@@ -31,7 +32,6 @@ use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
|
||||
EmailAddress,
|
||||
};
|
||||
|
||||
mod transfer;
|
||||
@@ -499,7 +499,7 @@ fn get_next_backup_path(
|
||||
backup_time: i64,
|
||||
) -> Result<(PathBuf, PathBuf, PathBuf)> {
|
||||
let folder = PathBuf::from(folder);
|
||||
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
|
||||
let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
|
||||
.context("can't get next backup path")?
|
||||
// Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
|
||||
.format("delta-chat-backup-%Y-%m-%d")
|
||||
@@ -1158,7 +1158,8 @@ mod tests {
|
||||
// Send a message that cannot be decrypted because the keys are
|
||||
// not synchronized yet.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
let trashed_message = alice.recv_msg_opt(&sent).await;
|
||||
assert!(trashed_message.is_none());
|
||||
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
|
||||
|
||||
// Transfer the key.
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
@@ -18,7 +19,7 @@ use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::tools::{self, time_elapsed, EmailAddress};
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
/// Convenience trait for working with keys.
|
||||
///
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
use crate::{chatlist_events, stock_str};
|
||||
|
||||
/// Location record.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -137,7 +137,7 @@ impl Kml {
|
||||
// 0 4 7 10 13 16 19
|
||||
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
|
||||
Ok(res) => {
|
||||
self.curr.timestamp = res.timestamp();
|
||||
self.curr.timestamp = res.and_utc().timestamp();
|
||||
let now = time();
|
||||
if self.curr.timestamp > now {
|
||||
self.curr.timestamp = now;
|
||||
@@ -290,6 +290,7 @@ pub async fn send_locations_to_chat(
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
if 0 != seconds {
|
||||
context.scheduler.interrupt_location().await;
|
||||
}
|
||||
@@ -540,7 +541,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(Strin
|
||||
|
||||
fn get_kml_timestamp(utc: i64) -> String {
|
||||
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
|
||||
chrono::NaiveDateTime::from_timestamp_opt(utc, 0)
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m-%dT%H:%M:%SZ")
|
||||
.to_string()
|
||||
@@ -802,6 +803,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ impl LoginParam {
|
||||
// Only check for IMAP password, SMTP password is an "advanced" setting.
|
||||
ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
|
||||
if param.smtp.password.is_empty() {
|
||||
param.smtp.password = param.imap.password.clone()
|
||||
param.smtp.password.clone_from(¶m.imap.password)
|
||||
}
|
||||
Ok(param)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
@@ -138,6 +139,7 @@ WHERE id=?;
|
||||
chat_id,
|
||||
msg_id: self,
|
||||
});
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -201,12 +203,14 @@ WHERE id=?;
|
||||
let fts = timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {fts}");
|
||||
|
||||
let name = Contact::get_by_id(context, msg.from_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
.unwrap_or_default();
|
||||
|
||||
ret += &format!(" by {name}");
|
||||
let from_contact = Contact::get_by_id(context, msg.from_id).await?;
|
||||
let name = from_contact.get_name_n_addr();
|
||||
if let Some(override_sender_name) = msg.get_override_sender_name() {
|
||||
let addr = from_contact.get_addr();
|
||||
ret += &format!(" by ~{override_sender_name} ({addr})");
|
||||
} else {
|
||||
ret += &format!(" by {name}");
|
||||
}
|
||||
ret += "\n";
|
||||
|
||||
if msg.from_id != ContactId::SELF {
|
||||
@@ -360,7 +364,7 @@ impl rusqlite::types::FromSql for MsgId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
// Would be nice if we could use match here, but alas.
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
if 0 <= val && val <= i64::from(u32::MAX) {
|
||||
Ok(MsgId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -457,7 +461,19 @@ impl Message {
|
||||
}
|
||||
|
||||
/// Loads message with given ID from the database.
|
||||
///
|
||||
/// Returns an error if the message does not exist.
|
||||
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
|
||||
let message = Self::load_from_db_optional(context, id)
|
||||
.await?
|
||||
.with_context(|| format!("Message {id} does not exist"))?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Loads message with given ID from the database.
|
||||
///
|
||||
/// Returns `None` if the message does not exist.
|
||||
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
|
||||
ensure!(
|
||||
!id.is_special(),
|
||||
"Can not load special message ID {} from DB",
|
||||
@@ -465,7 +481,7 @@ impl Message {
|
||||
);
|
||||
let msg = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS id,",
|
||||
@@ -492,7 +508,7 @@ impl Message {
|
||||
" m.location_id AS location,",
|
||||
" c.blocked AS blocked",
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=?;"
|
||||
" WHERE m.id=? AND chat_id!=3;"
|
||||
),
|
||||
(id,),
|
||||
|row| {
|
||||
@@ -794,7 +810,7 @@ impl Message {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
|
||||
Summary::new(context, self, chat, contact.as_ref()).await
|
||||
}
|
||||
|
||||
// It's a little unfortunate that the UI has to first call `dc_msg_get_override_sender_name` and then if it was `NULL`, call
|
||||
@@ -1131,13 +1147,8 @@ impl Message {
|
||||
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
return if msg.chat_id.is_trash() {
|
||||
// If message is already moved to trash chat, pretend it does not exist.
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg))
|
||||
};
|
||||
let msg = Message::load_from_db_optional(context, msg_id).await?;
|
||||
return Ok(msg);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1518,9 +1529,12 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// Run housekeeping to delete unused blobs.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
@@ -1655,6 +1669,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1715,6 +1730,7 @@ pub(crate) async fn set_msg_failed(
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1867,8 +1883,7 @@ pub(crate) async fn get_latest_by_rfc724_mids(
|
||||
) -> Result<Option<Message>> {
|
||||
for id in mids.iter().rev() {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
if msg.chat_id != DC_CHAT_ID_TRASH {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
}
|
||||
@@ -1973,8 +1988,9 @@ mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, marknoticed_chat, ChatItem};
|
||||
use crate::chat::{self, marknoticed_chat, send_text_msg, ChatItem};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
@@ -2016,7 +2032,7 @@ mod tests {
|
||||
assert_eq!(_msg2.get_filemime(), None);
|
||||
}
|
||||
|
||||
/// Tests that message cannot be prepared if account has no configured address.
|
||||
/// Tests that message can be prepared even if account has no configured address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prepare_not_configured() {
|
||||
let d = test::TestContext::new().await;
|
||||
@@ -2026,7 +2042,7 @@ mod tests {
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err());
|
||||
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2464,6 +2480,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_message_summary_text() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
// message summary does not change when reactions are applied (in contrast to chatlist summary)
|
||||
send_reaction(&t, msg_id, "🫵").await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_format_flowed_round_trip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -10,7 +10,7 @@ use lettre_email::{Address, Header, MimeMultipartType, PartBuilder};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::Chat;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::Contact;
|
||||
@@ -64,7 +64,15 @@ pub struct MimeFactory<'a> {
|
||||
loaded: Loaded,
|
||||
msg: &'a Message,
|
||||
in_reply_to: String,
|
||||
|
||||
/// Space-separated list of Message-IDs for `References` header.
|
||||
///
|
||||
/// Each Message-ID in the list
|
||||
/// may or may not be enclosed in angle brackets,
|
||||
/// angle brackets must be added during message rendering
|
||||
/// as needed.
|
||||
references: String,
|
||||
|
||||
req_mdn: bool,
|
||||
last_added_location_id: Option<u32>,
|
||||
|
||||
@@ -75,7 +83,7 @@ pub struct MimeFactory<'a> {
|
||||
sync_ids_to_delete: Option<String>,
|
||||
|
||||
/// True if the avatar should be attached.
|
||||
attach_selfavatar: bool,
|
||||
pub attach_selfavatar: bool,
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
@@ -85,7 +93,6 @@ pub struct RenderedEmail {
|
||||
// pub envelope: Envelope,
|
||||
pub is_encrypted: bool,
|
||||
pub is_gossiped: bool,
|
||||
pub is_group: bool,
|
||||
pub last_added_location_id: Option<u32>,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email
|
||||
@@ -130,11 +137,7 @@ struct MessageHeaders {
|
||||
}
|
||||
|
||||
impl<'a> MimeFactory<'a> {
|
||||
pub async fn from_msg(
|
||||
context: &Context,
|
||||
msg: &'a Message,
|
||||
attach_selfavatar: bool,
|
||||
) -> Result<MimeFactory<'a>> {
|
||||
pub async fn from_msg(context: &Context, msg: &'a Message) -> Result<MimeFactory<'a>> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
@@ -209,6 +212,7 @@ impl<'a> MimeFactory<'a> {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let attach_selfavatar = Self::should_attach_selfavatar(context, msg).await;
|
||||
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
@@ -384,6 +388,31 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_attach_selfavatar(context: &Context, msg: &Message) -> bool {
|
||||
let cmd = msg.param.get_cmd();
|
||||
(cmd != SystemMessage::SecurejoinMessage || {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
// Don't attach selfavatar at the earlier SecureJoin steps:
|
||||
// - The corresponding messages, i.e. "v{c,g}-request" and "v{c,g}-auth-required" are
|
||||
// deleted right after processing, so other devices won't see the avatar.
|
||||
// - It's also good for privacy because the contact isn't yet verified and these
|
||||
// messages are auto-sent unlike usual unencrypted messages.
|
||||
step == "vg-request-with-auth"
|
||||
|| step == "vc-request-with-auth"
|
||||
|| step == "vg-member-added"
|
||||
|| step == "vc-contact-confirm"
|
||||
}) && match chat::shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(should) => should,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"should_attach_selfavatar: cannot get selfavatar state: {err:#}."
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grpimage(&self) -> Option<String> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
@@ -545,18 +574,14 @@ impl<'a> MimeFactory<'a> {
|
||||
.protected
|
||||
.push(Header::new("Subject".into(), encoded_subject));
|
||||
|
||||
let date = chrono::Utc
|
||||
.from_local_datetime(
|
||||
&chrono::NaiveDateTime::from_timestamp_opt(self.timestamp, 0)
|
||||
.context("can't convert timestamp to NativeDateTime")?,
|
||||
)
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
headers.unprotected.push(Header::new("Date".into(), date));
|
||||
|
||||
let rfc724_mid = match self.loaded {
|
||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(&self.from_addr),
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
||||
};
|
||||
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
|
||||
let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
|
||||
@@ -588,8 +613,6 @@ impl<'a> MimeFactory<'a> {
|
||||
));
|
||||
}
|
||||
|
||||
let mut is_group = false;
|
||||
|
||||
if let Loaded::Message { chat } = &self.loaded {
|
||||
if chat.typ == Chattype::Broadcast {
|
||||
let encoded_chat_name = encode_words(&chat.name);
|
||||
@@ -597,8 +620,6 @@ impl<'a> MimeFactory<'a> {
|
||||
"List-ID".into(),
|
||||
format!("{encoded_chat_name} <{}>", chat.grpid),
|
||||
));
|
||||
} else if chat.typ == Chattype::Group {
|
||||
is_group = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,7 +891,6 @@ impl<'a> MimeFactory<'a> {
|
||||
// envelope: Envelope::new,
|
||||
is_encrypted,
|
||||
is_gossiped,
|
||||
is_group,
|
||||
last_added_location_id,
|
||||
sync_ids_to_delete: self.sync_ids_to_delete,
|
||||
rfc724_mid,
|
||||
@@ -1562,18 +1582,18 @@ fn maybe_encode_words(words: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
use std::str;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactAddress, Origin};
|
||||
use crate::contact::Origin;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
@@ -1828,7 +1848,7 @@ mod tests {
|
||||
Original-Message-ID: <2893@example.com>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n", &t).await;
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
|
||||
// The subject string should not be "Re: message opened"
|
||||
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
|
||||
}
|
||||
@@ -1963,7 +1983,7 @@ mod tests {
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
||||
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
|
||||
|
||||
mf.subject_str(&t).await.unwrap()
|
||||
}
|
||||
@@ -2048,7 +2068,7 @@ mod tests {
|
||||
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap();
|
||||
mf.subject_str(&t).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -2096,7 +2116,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(&t, &msg, false).await.unwrap();
|
||||
let mimefactory = MimeFactory::from_msg(&t, &msg).await.unwrap();
|
||||
|
||||
let recipients = mimefactory.recipients();
|
||||
assert_eq!(recipients, vec!["charlie@example.com"]);
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
use std::cmp::min;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::str;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, addr_normalize, strip_rtlo_characters};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use format_flowed::unformat_flowed;
|
||||
use lettre_email::mime::Mime;
|
||||
@@ -17,8 +16,8 @@ use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{add_info_msg, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
|
||||
use crate::contact::{addr_cmp, addr_normalize, Contact, ContactId, Origin};
|
||||
use crate::constants::{self, Chattype, DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{
|
||||
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
|
||||
@@ -34,13 +33,11 @@ use crate::message::{
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{
|
||||
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time,
|
||||
strip_rtlo_characters, truncate_by_lines,
|
||||
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_by_lines,
|
||||
};
|
||||
use crate::{location, tools};
|
||||
use crate::{chatlist_events, location, stock_str, tools};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -214,7 +211,9 @@ impl MimeMessage {
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.map_or(timestamp_rcvd, |value| min(value, timestamp_rcvd + 60));
|
||||
.map_or(timestamp_rcvd, |value| {
|
||||
min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
|
||||
});
|
||||
let mut hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
@@ -604,7 +603,7 @@ impl MimeMessage {
|
||||
let mut filepart = self.parts.swap_remove(1);
|
||||
|
||||
// insert new one
|
||||
filepart.msg = self.parts[0].msg.clone();
|
||||
filepart.msg.clone_from(&self.parts[0].msg);
|
||||
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
|
||||
filepart.param.set(Param::Quote, quote);
|
||||
}
|
||||
@@ -818,59 +817,53 @@ impl MimeMessage {
|
||||
self.headers.get(headerdef.get_headername())
|
||||
}
|
||||
|
||||
fn parse_mime_recursive<'a>(
|
||||
async fn parse_mime_recursive<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
mail: &'a mailparse::ParsedMail<'a>,
|
||||
is_related: bool,
|
||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + 'a + Send>> {
|
||||
use futures::future::FutureExt;
|
||||
) -> Result<bool> {
|
||||
enum MimeS {
|
||||
Multiple,
|
||||
Single,
|
||||
Message,
|
||||
}
|
||||
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
enum MimeS {
|
||||
Multiple,
|
||||
Single,
|
||||
Message,
|
||||
}
|
||||
let mimetype = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let mimetype = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let m = if mimetype.starts_with("multipart") {
|
||||
if mail.ctype.params.contains_key("boundary") {
|
||||
MimeS::Multiple
|
||||
} else {
|
||||
MimeS::Single
|
||||
}
|
||||
} else if mimetype.starts_with("message") {
|
||||
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
|
||||
MimeS::Message
|
||||
} else {
|
||||
MimeS::Single
|
||||
}
|
||||
let m = if mimetype.starts_with("multipart") {
|
||||
if mail.ctype.params.contains_key("boundary") {
|
||||
MimeS::Multiple
|
||||
} else {
|
||||
MimeS::Single
|
||||
};
|
||||
}
|
||||
} else if mimetype.starts_with("message") {
|
||||
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
|
||||
MimeS::Message
|
||||
} else {
|
||||
MimeS::Single
|
||||
}
|
||||
} else {
|
||||
MimeS::Single
|
||||
};
|
||||
|
||||
let is_related = is_related || mimetype == "multipart/related";
|
||||
match m {
|
||||
MimeS::Multiple => self.handle_multiple(context, mail, is_related).await,
|
||||
MimeS::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
let is_related = is_related || mimetype == "multipart/related";
|
||||
match m {
|
||||
MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
|
||||
MimeS::Message => {
|
||||
let raw = mail.get_body_raw()?;
|
||||
if raw.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
|
||||
|
||||
self.parse_mime_recursive(context, &mail, is_related).await
|
||||
}
|
||||
MimeS::Single => {
|
||||
self.add_single_part_if_known(context, mail, is_related)
|
||||
.await
|
||||
}
|
||||
Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
|
||||
}
|
||||
MimeS::Single => {
|
||||
self.add_single_part_if_known(context, mail, is_related)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
async fn handle_multiple(
|
||||
@@ -2159,6 +2152,8 @@ async fn handle_mdn(
|
||||
{
|
||||
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
|
||||
context.emit_event(EventType::MsgRead { chat_id, msg_id });
|
||||
// note(treefit): only matters if it is the last message in chat (but probably too expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
76
src/net.rs
76
src/net.rs
@@ -1,6 +1,6 @@
|
||||
//! # Common network utilities.
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
@@ -41,7 +41,8 @@ async fn lookup_host_with_timeout(
|
||||
|
||||
/// Looks up hostname and port using DNS and updates the address resolution cache.
|
||||
///
|
||||
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
|
||||
/// If `load_cache` is true, appends cached results not older than 30 days to the end
|
||||
/// or entries from fallback cache if there are no cached addresses.
|
||||
async fn lookup_host_with_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
@@ -129,11 +130,72 @@ async fn lookup_host_with_cache(
|
||||
//
|
||||
// In the future we may pre-resolve all provider database addresses
|
||||
// and build them in.
|
||||
if hostname == "mail.sangham.net" {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
port,
|
||||
));
|
||||
match hostname {
|
||||
"mail.sangham.net" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
"nine.testrun.org" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
"disroot.org" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
"mail.riseup.net" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
"imap.gmail.com" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
"smtp.gmail.com" => {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
|
||||
port,
|
||||
));
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/param.rs
32
src/param.rs
@@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::context::Context;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// Available param keys.
|
||||
@@ -18,7 +17,7 @@ use crate::mimeparser::SystemMessage;
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Param {
|
||||
/// For messages and jobs
|
||||
/// For messages
|
||||
File = b'f',
|
||||
|
||||
/// For messages: original filename (as shown in chat)
|
||||
@@ -65,6 +64,15 @@ pub enum Param {
|
||||
/// For Messages: the message is a reaction.
|
||||
Reaction = b'x',
|
||||
|
||||
/// For Chats: the timestamp of the last reaction.
|
||||
LastReactionTimestamp = b'y',
|
||||
|
||||
/// For Chats: Message ID of the last reaction.
|
||||
LastReactionMsgId = b'Y',
|
||||
|
||||
/// For Chats: Contact ID of the last reaction.
|
||||
LastReactionContactId = b'1',
|
||||
|
||||
/// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
|
||||
Bot = b'b',
|
||||
|
||||
@@ -107,18 +115,12 @@ pub enum Param {
|
||||
/// is used to also send all the forwarded messages.
|
||||
PrepForwards = b'P',
|
||||
|
||||
/// For Jobs
|
||||
/// For Messages
|
||||
SetLatitude = b'l',
|
||||
|
||||
/// For Jobs
|
||||
/// For Messages
|
||||
SetLongitude = b'n',
|
||||
|
||||
/// For Jobs
|
||||
AlsoMove = b'M',
|
||||
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
|
||||
/// For Groups
|
||||
///
|
||||
/// An unpromoted group has not had any messages sent to it and thus only exists on the
|
||||
@@ -173,9 +175,6 @@ pub enum Param {
|
||||
/// For Chats: timestamp of member list update.
|
||||
MemberListTimestamp = b'k',
|
||||
|
||||
/// For Chats: timestamp of protection settings update.
|
||||
ProtectionSettingsTimestamp = b'L',
|
||||
|
||||
/// For Webxdc Message Instances: Current document name
|
||||
WebxdcDocument = b'R',
|
||||
|
||||
@@ -190,6 +189,7 @@ pub enum Param {
|
||||
|
||||
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
||||
ForceSticker = b'X',
|
||||
// 'L' was defined as ProtectionSettingsTimestamp for Chats, however, never used in production.
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
@@ -399,12 +399,6 @@ impl Params {
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
pub fn get_msg_id(&self) -> Option<MsgId> {
|
||||
self.get(Param::MsgId)
|
||||
.and_then(|x| x.parse().ok())
|
||||
.map(MsgId::new)
|
||||
}
|
||||
|
||||
/// Set the given parameter to the passed in `i32`.
|
||||
pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
|
||||
self.set(key, format!("{value}"));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, ContactAddress};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
@@ -10,14 +11,14 @@ use crate::chat::{self, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{addr_cmp, Contact, ContactAddress, Origin};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str;
|
||||
use crate::{chatlist_events, stock_str};
|
||||
|
||||
/// Type of the public key stored inside the peerstate.
|
||||
#[derive(Debug)]
|
||||
@@ -722,6 +723,9 @@ impl Peerstate {
|
||||
.await?;
|
||||
}
|
||||
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// update the chats the contact is part of
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -784,7 +788,7 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
.await?;
|
||||
|
||||
let old_addr = mem::take(&mut peerstate.addr);
|
||||
peerstate.addr = info.from.clone();
|
||||
peerstate.addr.clone_from(&info.from);
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
@@ -20,7 +21,6 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
mod data;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// Provider status according to manual testing.
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use serde::Deserialize;
|
||||
@@ -13,9 +14,7 @@ use self::dclogin_scheme::configure_from_login_qr;
|
||||
use crate::chat::{get_chat_id_by_grpid, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{
|
||||
addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
use super::{Qr, DCLOGIN_SCHEME};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::CertificateChecks;
|
||||
use crate::provider::Socket;
|
||||
use crate::{contact, login_param::CertificateChecks};
|
||||
|
||||
/// Options for `dclogin:` scheme.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -88,7 +90,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
.collect();
|
||||
|
||||
// check if username is there
|
||||
if !contact::may_be_valid_addr(addr) {
|
||||
if !may_be_valid_addr(addr) {
|
||||
bail!("invalid DCLOGIN payload: invalid username E5");
|
||||
}
|
||||
|
||||
|
||||
258
src/reaction.rs
258
src/reaction.rs
@@ -20,11 +20,13 @@ use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::chat::{send_msg, ChatId};
|
||||
use crate::chat::{send_msg, Chat, ChatId};
|
||||
use crate::chatlist_events;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
|
||||
/// A single reaction consisting of multiple emoji sequences.
|
||||
///
|
||||
@@ -170,6 +172,7 @@ async fn set_msg_id_reaction(
|
||||
msg_id: MsgId,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
) -> Result<()> {
|
||||
if reaction.is_empty() {
|
||||
@@ -194,6 +197,17 @@ async fn set_msg_id_reaction(
|
||||
(msg_id, contact_id, reaction.as_str()),
|
||||
)
|
||||
.await?;
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::LastReactionTimestamp, timestamp)?
|
||||
{
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
|
||||
chat.param
|
||||
.set_i64(Param::LastReactionContactId, i64::from(contact_id.to_u32()));
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
}
|
||||
|
||||
context.emit_event(EventType::ReactionsChanged {
|
||||
@@ -201,6 +215,7 @@ async fn set_msg_id_reaction(
|
||||
msg_id,
|
||||
contact_id,
|
||||
});
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -223,7 +238,15 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
|
||||
|
||||
// Only set reaction if we successfully sent the message.
|
||||
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
|
||||
set_msg_id_reaction(
|
||||
context,
|
||||
msg_id,
|
||||
msg.chat_id,
|
||||
ContactId::SELF,
|
||||
reaction_msg.timestamp_sort,
|
||||
reaction,
|
||||
)
|
||||
.await?;
|
||||
Ok(reaction_msg_id)
|
||||
}
|
||||
|
||||
@@ -250,10 +273,11 @@ pub(crate) async fn set_msg_reaction(
|
||||
in_reply_to: &str,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
) -> Result<()> {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
@@ -307,18 +331,72 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
|
||||
Ok(Reactions { reactions })
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
/// Check if there is a reaction newer than the given timestamp.
|
||||
///
|
||||
/// If so, reaction details are returned and can be used to create a summary string.
|
||||
pub async fn get_last_reaction_if_newer_than(
|
||||
&self,
|
||||
context: &Context,
|
||||
timestamp: i64,
|
||||
) -> Result<Option<(Message, ContactId, String)>> {
|
||||
if self
|
||||
.param
|
||||
.get_i64(Param::LastReactionTimestamp)
|
||||
.filter(|&reaction_timestamp| reaction_timestamp > timestamp)
|
||||
.is_none()
|
||||
{
|
||||
return Ok(None);
|
||||
};
|
||||
let reaction_msg_id = MsgId::new(
|
||||
self.param
|
||||
.get_int(Param::LastReactionMsgId)
|
||||
.unwrap_or_default() as u32,
|
||||
);
|
||||
let Some(reaction_msg) = Message::load_from_db_optional(context, reaction_msg_id).await?
|
||||
else {
|
||||
// The message reacted to may be deleted.
|
||||
// These are no errors as `Param::LastReaction*` are just weak pointers.
|
||||
// Instead, just return `Ok(None)` and let the caller create another summary.
|
||||
return Ok(None);
|
||||
};
|
||||
let reaction_contact_id = ContactId::new(
|
||||
self.param
|
||||
.get_int(Param::LastReactionContactId)
|
||||
.unwrap_or_default() as u32,
|
||||
);
|
||||
if let Some(reaction) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?",
|
||||
(reaction_msg.id, reaction_contact_id),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(Some((reaction_msg, reaction_contact_id, reaction)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::get_chat_msgs;
|
||||
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::contact::{Contact, ContactAddress, Origin};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::download::DownloadState;
|
||||
use crate::message::MessageState;
|
||||
use crate::message::{delete_msgs, MessageState};
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_parse_reaction() {
|
||||
@@ -518,8 +596,7 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
|
||||
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
|
||||
alice.recv_msg_trash(&bob_reaction_msg).await;
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
|
||||
|
||||
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||
@@ -549,6 +626,145 @@ Here's my footer -- bob@example.net"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_summary(t: &TestContext, expected: &str) {
|
||||
let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
|
||||
let summary = chatlist.get_summary(t, 0, None).await.unwrap();
|
||||
assert_eq!(summary.text, expected);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_summary() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice.set_config(Config::Displayname, Some("ALICE")).await?;
|
||||
bob.set_config(Config::Displayname, Some("BOB")).await?;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
|
||||
let bob_msg1 = bob.recv_msg(&alice_msg1).await;
|
||||
|
||||
// Bob reacts to Alice's message, this is shown in the summaries
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
bob_msg1.chat_id.accept(&bob).await?;
|
||||
send_reaction(&bob, bob_msg1.id, "👍").await?;
|
||||
let bob_send_reaction = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_send_reaction).await;
|
||||
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
assert_eq!(summary.text, "You reacted 👍 to \"Party?\"");
|
||||
assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
|
||||
assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
|
||||
assert!(summary.prefix.is_none());
|
||||
assert!(summary.thumbnail_path.is_none());
|
||||
assert_summary(&alice, "BOB reacted 👍 to \"Party?\"").await;
|
||||
|
||||
// Alice reacts to own message as well
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
|
||||
let alice_send_reaction = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_opt(&alice_send_reaction).await;
|
||||
|
||||
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
|
||||
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
|
||||
|
||||
// Alice sends a newer message, this overwrites reaction summaries
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
|
||||
bob.recv_msg(&alice_msg2).await;
|
||||
|
||||
assert_summary(&alice, "kewl").await;
|
||||
assert_summary(&bob, "kewl").await;
|
||||
|
||||
// Reactions to older messages still overwrite newer messages
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🤘").await?;
|
||||
let alice_send_reaction = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_opt(&alice_send_reaction).await;
|
||||
|
||||
assert_summary(&alice, "You reacted 🤘 to \"Party?\"").await;
|
||||
assert_summary(&bob, "ALICE reacted 🤘 to \"Party?\"").await;
|
||||
|
||||
// Retracted reactions remove all summary reactions
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
|
||||
let alice_remove_reaction = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_opt(&alice_remove_reaction).await;
|
||||
|
||||
assert_summary(&alice, "kewl").await;
|
||||
assert_summary(&bob, "kewl").await;
|
||||
|
||||
// Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🧹").await?;
|
||||
assert_summary(&alice, "You reacted 🧹 to \"Party?\"").await;
|
||||
|
||||
delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
|
||||
assert_summary(&alice, "kewl").await;
|
||||
housekeeping(&alice).await?; // this will delete the tombstone
|
||||
assert_summary(&alice, "kewl").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_forwarded_summary() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice adds a message to "Saved Messages"
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
|
||||
assert_summary(&alice, "foo").await;
|
||||
|
||||
// Alice reacts to that message
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, msg_id, "🐫").await?;
|
||||
assert_summary(&alice, "You reacted 🐫 to \"foo\"").await;
|
||||
let reactions = get_msg_reactions(&alice, msg_id).await?;
|
||||
assert_eq!(reactions.reactions.len(), 1);
|
||||
|
||||
// Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
|
||||
forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
|
||||
assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
|
||||
let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
|
||||
let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
|
||||
let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
|
||||
assert!(reactions.reactions.is_empty()); // reactions are not forwarded
|
||||
|
||||
// Alice reacts to forwarded message:
|
||||
// For reaction summary neither original message author nor "Forwarded" prefix is shown
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice, forwarded_msg_id, "🐳").await?;
|
||||
assert_summary(&alice, "You reacted 🐳 to \"foo\"").await;
|
||||
let reactions = get_msg_reactions(&alice, msg_id).await?;
|
||||
assert_eq!(reactions.reactions.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let chat = alice0.get_self_chat().await;
|
||||
|
||||
let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
|
||||
alice1.recv_msg(&alice0.pop_sent_msg().await).await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
send_reaction(&alice0, msg_id, "👆").await?;
|
||||
let sync = alice0.pop_sent_msg().await;
|
||||
receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
|
||||
|
||||
assert_summary(&alice0, "You reacted 👆 to \"mom's birthday!\"").await;
|
||||
assert_summary(&alice1, "You reacted 👆 to \"mom's birthday!\"").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_and_reaction() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -591,7 +807,7 @@ Here's my footer -- bob@example.net"
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives a reaction.
|
||||
alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
|
||||
alice.recv_msg_trash(&bob_reaction_msg).await;
|
||||
|
||||
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
@@ -643,7 +859,7 @@ Here's my footer -- bob@example.net"
|
||||
{
|
||||
send_reaction(&alice2, alice2_msg.id, "👍").await?;
|
||||
let msg = alice2.pop_sent_msg().await;
|
||||
alice1.recv_msg(&msg).await;
|
||||
alice1.recv_msg_trash(&msg).await;
|
||||
}
|
||||
|
||||
// Check that the status is still the same.
|
||||
@@ -653,4 +869,24 @@ Here's my footer -- bob@example.net"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction_multidevice() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let bob_id = Contact::create(&alice0, "", "bob@example.net").await?;
|
||||
let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?;
|
||||
|
||||
let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
|
||||
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
|
||||
|
||||
send_reaction(&alice0, alice0_msg_id, "👀").await?;
|
||||
alice1.recv_msg_trash(&alice0.pop_sent_msg().await).await;
|
||||
|
||||
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
|
||||
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::{
|
||||
addr_cmp, may_be_valid_addr, normalize_name, strip_rtlo_characters, ContactAddress,
|
||||
};
|
||||
use mailparse::{parse_mail, SingleInfo};
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -11,10 +14,8 @@ use regex::Regex;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact::{
|
||||
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
|
||||
};
|
||||
use crate::constants::{self, Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
use crate::download::DownloadState;
|
||||
@@ -22,7 +23,6 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
|
||||
@@ -37,9 +37,8 @@ use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{
|
||||
self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters, validate_id,
|
||||
};
|
||||
use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, validate_id};
|
||||
use crate::{chatlist_events, location};
|
||||
use crate::{contact, imap};
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
@@ -316,6 +315,9 @@ pub(crate) async fn receive_imf_inner(
|
||||
//
|
||||
// If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in
|
||||
// a mailing list the sender displayname sometimes does not belong to the sender email address.
|
||||
// For example, GitHub sends messages from `notifications@github.com`,
|
||||
// but uses display name of the user whose action generated the notification
|
||||
// as the display name.
|
||||
let (from_id, _from_id_blocked, incoming_origin) =
|
||||
match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
|
||||
Some(contact_id_res) => contact_id_res,
|
||||
@@ -473,11 +475,46 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
if let Some(ref status_update) = mime_parser.webxdc_status_update {
|
||||
if let Err(err) = context
|
||||
.receive_status_update(from_id, insert_msg_id, status_update)
|
||||
.await
|
||||
let can_info_msg;
|
||||
let instance = if mime_parser
|
||||
.parts
|
||||
.first()
|
||||
.filter(|part| part.typ == Viewtype::Webxdc)
|
||||
.is_some()
|
||||
{
|
||||
warn!(context, "receive_imf cannot update status: {err:#}.");
|
||||
can_info_msg = false;
|
||||
Some(Message::load_from_db(context, insert_msg_id).await?)
|
||||
} else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
if let Some(instance) = get_rfc724_mid_in_list(context, field).await? {
|
||||
can_info_msg = instance.download_state() == DownloadState::Done;
|
||||
Some(instance)
|
||||
} else {
|
||||
can_info_msg = false;
|
||||
None
|
||||
}
|
||||
} else {
|
||||
can_info_msg = false;
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(instance) = instance {
|
||||
if let Err(err) = context
|
||||
.receive_status_update(
|
||||
from_id,
|
||||
&instance,
|
||||
received_msg.sort_timestamp,
|
||||
can_info_msg,
|
||||
status_update,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "receive_imf cannot update status: {err:#}.");
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Received webxdc update, but cannot assign it to message."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,12 +665,9 @@ pub async fn from_field_to_contact_id(
|
||||
if from_id == ContactId::SELF {
|
||||
Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
|
||||
} else {
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
|
||||
from_id_blocked = contact.blocked;
|
||||
incoming_origin = contact.origin;
|
||||
}
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
let from_id_blocked = contact.blocked;
|
||||
let incoming_origin = contact.origin;
|
||||
Ok(Some((from_id, from_id_blocked, incoming_origin)))
|
||||
}
|
||||
}
|
||||
@@ -1077,6 +1111,12 @@ async fn add_parts(
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
}
|
||||
if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
|
||||
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
}
|
||||
|
||||
// automatically unblock chat when the user sends a message
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
@@ -1374,6 +1414,7 @@ async fn add_parts(
|
||||
&mime_in_reply_to,
|
||||
orig_chat_id.unwrap_or_default(),
|
||||
from_id,
|
||||
sort_timestamp,
|
||||
Reaction::from(reaction_str.as_str()),
|
||||
)
|
||||
.await?;
|
||||
@@ -1742,7 +1783,19 @@ async fn create_or_lookup_group(
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
|
||||
grpid
|
||||
} else if allow_creation {
|
||||
} else if !allow_creation {
|
||||
info!(context, "Creating ad-hoc group prevented from caller.");
|
||||
return Ok(None);
|
||||
} else if is_partial_download {
|
||||
// Partial download may be an encrypted message with protected Subject header.
|
||||
//
|
||||
// We do not want to create a group with "..." or "Encrypted message" as a subject.
|
||||
info!(
|
||||
context,
|
||||
"Ad-hoc group cannot be created from partial download."
|
||||
);
|
||||
return Ok(None);
|
||||
} else {
|
||||
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
|
||||
if !member_ids.contains(&(from_id)) {
|
||||
member_ids.push(from_id);
|
||||
@@ -1756,9 +1809,6 @@ async fn create_or_lookup_group(
|
||||
.context("could not create ad hoc group")?
|
||||
.map(|chat_id| (chat_id, create_blocked));
|
||||
return Ok(res);
|
||||
} else {
|
||||
info!(context, "Creating ad-hoc group prevented from caller.");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut chat_id;
|
||||
@@ -1852,6 +1902,8 @@ async fn create_or_lookup_group(
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
@@ -1912,18 +1964,24 @@ async fn apply_group_changes(
|
||||
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let is_from_in_chat =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = !is_partial_download
|
||||
&& is_from_in_chat
|
||||
&& chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let member_list_ts = match !is_partial_download && is_from_in_chat {
|
||||
true => Some(chat_id.get_member_list_timestamp(context).await?),
|
||||
false => None,
|
||||
};
|
||||
// When we remove a member locally, we shift `MemberListTimestamp` by `TIMESTAMP_SENT_TOLERANCE`
|
||||
// into the future, so add some more tolerance here to allow remote membership changes as well.
|
||||
let timestamp_sent_tolerance = constants::TIMESTAMP_SENT_TOLERANCE * 2;
|
||||
let allow_member_list_changes = member_list_ts
|
||||
.filter(|t| {
|
||||
*t <= mime_parser
|
||||
.timestamp_sent
|
||||
.saturating_add(timestamp_sent_tolerance)
|
||||
})
|
||||
.is_some();
|
||||
let sync_member_list = member_list_ts
|
||||
.filter(|t| *t <= mime_parser.timestamp_sent)
|
||||
.is_some();
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = {
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
@@ -1938,15 +1996,16 @@ async fn apply_group_changes(
|
||||
.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && {
|
||||
if !allow_member_list_changes {
|
||||
} && (
|
||||
// Don't allow the timestamp tolerance here for more reliable leaving of groups.
|
||||
sync_member_list || {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
|
||||
);
|
||||
false
|
||||
}
|
||||
allow_member_list_changes
|
||||
};
|
||||
);
|
||||
|
||||
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
|
||||
@@ -2059,6 +2118,13 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
if !recreate_member_list {
|
||||
let mut diff = HashSet::<ContactId>::new();
|
||||
if sync_member_list {
|
||||
diff = new_members.difference(&chat_contacts).copied().collect();
|
||||
} else if let Some(added_id) = added_id {
|
||||
diff.insert(added_id);
|
||||
}
|
||||
new_members.clone_from(&chat_contacts);
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
// - Classical MUA users usually don't intend to remove users from an email thread, so
|
||||
@@ -2070,9 +2136,6 @@ async fn apply_group_changes(
|
||||
// will likely recreate the member list from the next received message. The problem
|
||||
// occurs only if that "somebody" managed to reply earlier. Really, it's a problem for
|
||||
// big groups with high message rate, but let it be for now.
|
||||
let mut diff: HashSet<ContactId> =
|
||||
new_members.difference(&chat_contacts).copied().collect();
|
||||
new_members = chat_contacts.clone();
|
||||
new_members.extend(diff.clone());
|
||||
if let Some(added_id) = added_id {
|
||||
diff.remove(&added_id);
|
||||
@@ -2108,6 +2171,17 @@ async fn apply_group_changes(
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
if sync_member_list {
|
||||
let mut ts = mime_parser.timestamp_sent;
|
||||
if recreate_member_list {
|
||||
// Reject all older membership changes. See `allow_member_list_changes` to know how
|
||||
// this works.
|
||||
ts += timestamp_sent_tolerance;
|
||||
}
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, ts)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
@@ -2143,6 +2217,7 @@ async fn apply_group_changes(
|
||||
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
Ok((group_changes_msgs, better_msg))
|
||||
}
|
||||
@@ -2239,7 +2314,7 @@ fn compute_mailinglist_name(
|
||||
// and we can detect these lists by a unique `ListId`-suffix.
|
||||
if listid.ends_with(".list-id.mcsv.net") {
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
name.clone_from(display_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2267,7 +2342,7 @@ fn compute_mailinglist_name(
|
||||
|| listid.ends_with(".xt.local"))
|
||||
{
|
||||
if let Some(display_name) = &mime_parser.from.display_name {
|
||||
name = display_name.clone();
|
||||
name.clone_from(display_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2445,6 +2520,8 @@ async fn create_adhoc_group(
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
|
||||
|
||||
Ok(Some(new_chat_id))
|
||||
}
|
||||
@@ -2656,8 +2733,6 @@ async fn mark_recipients_as_verified(
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
///
|
||||
/// For Delta Chat messages it is the last message in the chat of the sender.
|
||||
///
|
||||
/// Note that the returned message may be trashed.
|
||||
async fn get_previous_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
@@ -2665,7 +2740,7 @@ async fn get_previous_message(
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
|
||||
if let Some(rfc724mid) = parse_message_ids(field).last() {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
|
||||
return Ok(Some(Message::load_from_db(context, msg_id).await?));
|
||||
return Message::load_from_db_optional(context, msg_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2683,7 +2758,6 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
|
||||
///
|
||||
/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
|
||||
/// References: header.
|
||||
// TODO also save first entry of References and look for this?
|
||||
async fn get_parent_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
|
||||
@@ -142,6 +142,35 @@ async fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(
|
||||
Config::ShowEmails,
|
||||
Some(&ShowEmails::AcceptedContacts.to_string()),
|
||||
)
|
||||
.await?;
|
||||
tcm.send_recv(alice, bob, "hi").await;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org, claire@example.com\n\
|
||||
Message-ID: <3333@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -3699,6 +3728,7 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
send_text_msg(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
@@ -3781,17 +3811,18 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "fiora", "fiora@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Alice didn't receive Bob's leave message, so Bob must readd themselves otherwise other
|
||||
// members would think Bob is still here while they aren't, and then retry to leave if they
|
||||
// Alice didn't receive Bob's leave message although a lot of time has
|
||||
// passed, so Bob must readd themselves otherwise other members would think
|
||||
// Bob is still here while they aren't. Bob should retry to leave if they
|
||||
// think that Alice didn't re-add them on purpose (which is possible if Alice uses a classical
|
||||
// MUA).
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
@@ -3983,8 +4014,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
|
||||
// which some members simply deleted and some members left,
|
||||
// recreating the chat for others.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
assert!(bob_chat_id.is_trash());
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4011,6 +4041,15 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
// Bob missed the message adding them, but must recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
// But if Bob just left, they mustn't recreate the member list even after missing a message.
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "3rd message".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4325,8 +4364,8 @@ async fn test_forged_from() -> Result<()> {
|
||||
.payload
|
||||
.replace("bob@example.net", "notbob@example.net");
|
||||
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert!(msg.chat_id.is_trash());
|
||||
let msg = alice.recv_msg_opt(&sent_msg).await;
|
||||
assert!(msg.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4351,3 +4390,64 @@ async fn test_multiline_iso_8859_1_subject() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_references() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
alice.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let _sent = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let bob_received_msg = bob.recv_msg(&sent).await;
|
||||
let bob_chat_id = bob_received_msg.chat_id;
|
||||
|
||||
// Alice sends another three messages, but two of them are lost.
|
||||
let _sent = alice.send_text(alice_chat_id, "Second message").await;
|
||||
let _sent = alice.send_text(alice_chat_id, "Third message").await;
|
||||
|
||||
// Message can still be assigned based on the `References` header.
|
||||
let sent = alice.send_text(alice_chat_id, "Fourth message").await;
|
||||
let bob_parsed_message = bob.parse_msg(&sent).await;
|
||||
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?.unwrap();
|
||||
assert_eq!(bob_chat_id, bob_parent_message.chat_id);
|
||||
|
||||
// If more messages are lost, message cannot be assigned to the correct chat
|
||||
// without `Chat-Group-ID` header, e.g. if the message is partially downloaded.
|
||||
let sent = alice.send_text(alice_chat_id, "Fifth message").await;
|
||||
let bob_parsed_message = bob.parse_msg(&sent).await;
|
||||
let bob_parent_message = get_parent_message(bob, &bob_parsed_message).await?;
|
||||
assert!(bob_parent_message.is_none());
|
||||
|
||||
// When the message is received, it is assigned correctly because of `Chat-Group-ID` header.
|
||||
let bob_received_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_chat_id, bob_received_msg.chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_list_from() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/list-from.eml");
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
|
||||
assert_eq!(msg.get_override_sender_name().unwrap(), "ÖAMTC");
|
||||
let sender_contact = Contact::get_by_id(t, msg.from_id).await?;
|
||||
assert_eq!(
|
||||
sender_contact.get_display_name(),
|
||||
"clubinfo@donotreply.oeamtc.at"
|
||||
);
|
||||
let info = msg.id.get_info(t).await?;
|
||||
assert!(info.contains(" by ~ÖAMTC (clubinfo@donotreply.oeamtc.at)"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -247,6 +247,14 @@ impl SchedulerState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt optional boxes (mvbox, sentbox) loops.
|
||||
pub(crate) async fn interrupt_oboxes(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.interrupt_oboxes();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
@@ -565,19 +573,28 @@ async fn fetch_idle(
|
||||
.context("store_seen_flags_on_imap")?;
|
||||
}
|
||||
|
||||
// Fetch the watched folder.
|
||||
connection
|
||||
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
if !ctx.should_delete_to_trash().await?
|
||||
|| ctx
|
||||
.get_config(Config::ConfiguredTrashFolder)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
// Fetch the watched folder.
|
||||
connection
|
||||
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
|
||||
// called right before `fetch_move_delete` because it is not well optimized and would
|
||||
// otherwise slow down message fetching.
|
||||
delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
} else if folder_config == Config::ConfiguredInboxFolder {
|
||||
ctx.last_full_folder_scan.lock().await.take();
|
||||
}
|
||||
|
||||
// Scan additional folders only after finishing fetching the watched folder.
|
||||
//
|
||||
@@ -911,6 +928,12 @@ impl Scheduler {
|
||||
self.inbox.conn_state.interrupt();
|
||||
}
|
||||
|
||||
fn interrupt_oboxes(&self) {
|
||||
for b in &self.oboxes {
|
||||
b.conn_state.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
}
|
||||
@@ -924,7 +947,7 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
fn interrupt_recently_seen(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.recently_seen_loop.interrupt(contact_id, timestamp);
|
||||
self.recently_seen_loop.try_interrupt(contact_id, timestamp);
|
||||
}
|
||||
|
||||
/// Halt the scheduler.
|
||||
|
||||
@@ -9,8 +9,8 @@ use tokio::sync::Mutex;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{scan_folders::get_watched_folder_configs, FolderMeaning};
|
||||
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
|
||||
use crate::stock_str;
|
||||
use crate::{context::Context, log::LogExt};
|
||||
use crate::{stock_str, tools};
|
||||
|
||||
use super::InnerSchedulerState;
|
||||
|
||||
@@ -170,7 +170,7 @@ impl ConnectivityStore {
|
||||
}
|
||||
|
||||
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
|
||||
/// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()`
|
||||
/// Called during `dc_maybe_network()` to make sure that `dc_all_work_done()`
|
||||
/// returns false immediately after `dc_maybe_network()`.
|
||||
pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
|
||||
let mut connectivity_lock = inbox.0.lock().await;
|
||||
@@ -413,7 +413,9 @@ impl Context {
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = &tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
|
||||
let domain =
|
||||
&deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
|
||||
.domain;
|
||||
let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
|
||||
ret += &format!("<h3>{storage_on_domain}</h3><ul>");
|
||||
let quota = self.quota.read().await;
|
||||
|
||||
@@ -5,6 +5,7 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
@@ -680,6 +681,7 @@ async fn secure_connection_established(
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -754,17 +756,18 @@ fn encrypted_and_signed(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::remove_contact_from_chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactAddress;
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::chat_protection_enabled;
|
||||
use crate::test_utils::get_chat_msg;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::{EmailAddress, SystemTime};
|
||||
use crate::tools::SystemTime;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -833,7 +836,7 @@ mod tests {
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
@@ -851,7 +854,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg(&sent).await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
@@ -933,7 +936,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
@@ -982,7 +985,7 @@ mod tests {
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg(&sent).await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
@@ -1083,7 +1086,7 @@ mod tests {
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -1104,7 +1107,7 @@ mod tests {
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg(&sent).await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
|
||||
Ok(())
|
||||
@@ -1182,7 +1185,7 @@ mod tests {
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
@@ -1193,7 +1196,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
bob.recv_msg(&sent).await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
@@ -1240,7 +1243,7 @@ mod tests {
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -1388,15 +1391,15 @@ First thread."#;
|
||||
|
||||
// vc-request
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-auth-required
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-request-with-auth
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg(&sent).await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// Alice has Bob verified now.
|
||||
let contact_bob_id =
|
||||
|
||||
@@ -806,13 +806,6 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM jobs;",
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
&context.sql,
|
||||
&mut files_in_use,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Migrations module.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -10,7 +11,6 @@ use crate::imap;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_by_domain;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
const DBVERSION: i32 = 68;
|
||||
const VERSION_CFG: &str = "dbversion";
|
||||
|
||||
@@ -429,6 +429,12 @@ pub enum StockMessage {
|
||||
fallback = "⚠️ 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."
|
||||
))]
|
||||
CantDecryptOutgoingMsgs = 175,
|
||||
|
||||
#[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
|
||||
MsgYouReacted = 176,
|
||||
|
||||
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
|
||||
MsgReactedBy = 177,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -730,6 +736,27 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
|
||||
pub(crate) async fn msg_reacted(
|
||||
context: &Context,
|
||||
by_contact: ContactId,
|
||||
reaction: &str,
|
||||
summary: &str,
|
||||
) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouReacted)
|
||||
.await
|
||||
.replace1(reaction)
|
||||
.replace2(summary)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgReactedBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
.replace2(reaction)
|
||||
.replace3(summary)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `GIF`.
|
||||
pub(crate) async fn gif(context: &Context) -> String {
|
||||
translated(context, StockMessage::Gif).await
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user