mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
1 Commits
v1.154.1
...
hoc/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61fb9e7e3b |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.84.0
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -37,10 +37,8 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
run: scripts/clippy.sh
|
||||
- name: Check with all features
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
- name: Check with only default features
|
||||
run: cargo check --all-targets
|
||||
|
||||
npm_constants:
|
||||
name: Check if node constants are up to date
|
||||
@@ -97,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.84.0
|
||||
rust: 1.82.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
@@ -152,7 +150,7 @@ jobs:
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi
|
||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -223,11 +221,11 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
python: 3.7
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -277,9 +275,9 @@ jobs:
|
||||
- os: macos-latest
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
# Minimum Supported Python Version = 3.7
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
python: 3.7
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
265
CHANGELOG.md
265
CHANGELOG.md
@@ -1,254 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.154.1] - 2025-01-15
|
||||
|
||||
### Tests
|
||||
|
||||
- Expect trashing of no-op "member added" in non_member_cannot_modify_member_list.
|
||||
|
||||
## [1.154.0] - 2025-01-15
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- New group consistency algorithm.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Migration: Set bcc_self=1 if it's unset and delete_server_after!=1 ([#6432](https://github.com/deltachat/deltachat-core-rust/pull/6432)).
|
||||
- Clear the config cache after every migration ([#6438](https://github.com/deltachat/deltachat-core-rust/pull/6438)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase minimum supported Python version to 3.8.
|
||||
- [**breaking**] Remove jsonrpc feature flag.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.84.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Beta Clippy suggestions ([#6422](https://github.com/deltachat/deltachat-core-rust/pull/6422)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use let..else.
|
||||
- Add why_cant_send_ex() capable to only ignore specified conditions.
|
||||
- Remove unnecessary is_contact_in_chat check.
|
||||
- Eliminate remaining repeat_vars() calls ([#6359](https://github.com/deltachat/deltachat-core-rust/pull/6359)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Use assert_eq! to compare chatlist length.
|
||||
|
||||
## [1.153.0] - 2025-01-05
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove "jobs" from imap_markseen if folder doesn't exist ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
|
||||
- Delete `vg-request-with-auth` from IMAP after processing ([#6208](https://github.com/deltachat/deltachat-core-rust/pull/6208)).
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `IncomingWebxdcNotify.chat_id` ([#6356](https://github.com/deltachat/deltachat-core-rust/pull/6356)).
|
||||
- rpc-client: Add INCOMING_REACTION to const.EventType ([#6349](https://github.com/deltachat/deltachat-core-rust/pull/6349)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Viewtype::Sticker may be changed to Image and how to disable that ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never change Viewtype::Sticker to Image if file has non-image extension ([#6352](https://github.com/deltachat/deltachat-core-rust/pull/6352)).
|
||||
- Change BccSelf default to 0 for chatmail ([#6340](https://github.com/deltachat/deltachat-core-rust/pull/6340)).
|
||||
- Mark holiday notice messages as bot-generated.
|
||||
- Don't treat location-only and sync messages as bot ones ([#6357](https://github.com/deltachat/deltachat-core-rust/pull/6357)).
|
||||
- Update shadowsocks crate to 1.22.0 to avoid panic when parsing some QR codes.
|
||||
- Prefer to encrypt if E2eeEnabled even if peers have EncryptPreference::NoPreference.
|
||||
- Prioritize mailing list over self-sent messages.
|
||||
- Allow empty `To` field for self-sent messages.
|
||||
- Default `to_id` to self instead of 0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused parameter and return value from `build_body_file(…)` ([#6369](https://github.com/deltachat/deltachat-core-rust/pull/6369)).
|
||||
- Deprecate Param::ErroneousE2ee.
|
||||
- Add `emit_msgs_changed_without_msg_id`.
|
||||
- Add_parts: Remove excessive `is_mdn` checks.
|
||||
- Simplify `self_sent` condition.
|
||||
- Don't ignore get_for_contact errors.
|
||||
|
||||
### Tests
|
||||
|
||||
- Messages without recipients are assigned to self chat.
|
||||
- Message with empty To: field should have a valid to_id.
|
||||
- Fix `test_logged_ac_process_ffi_failure` flakiness.
|
||||
|
||||
## [1.152.2] - 2024-12-24
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Emit ImexProgress(1) after receiving backup size.
|
||||
- `delete_msgs`: Use `transaction()` instead of `call_write()`.
|
||||
- Start ephemeral timers when the chat is noticed.
|
||||
- Start ephemeral timers when the chat is archived.
|
||||
- Revalidate HTTP cache entries once per minute maximum.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reduce number of `repeat_vars()` calls.
|
||||
- `sanitise_name`: Don't consider punctuation and control chars as part of file extension ([#6362](https://github.com/deltachat/deltachat-core-rust/pull/6362)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove marknoticed_chat_if_older_than().
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove contrib/ directory.
|
||||
|
||||
## [1.152.1] - 2024-12-17
|
||||
|
||||
### Build system
|
||||
|
||||
- Downgrade Rust version used to build binaries.
|
||||
- Reduce MSRV to 1.77.0.
|
||||
|
||||
## [1.152.0] - 2024-12-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `dc_prepare_msg` and `dc_msg_is_increation`.
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase MSRV to 1.81.0.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Cache HTTP GET requests.
|
||||
- Prefix server-url in info.
|
||||
- Set `mime_modified` for the last message part, not the first ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Render "message" parts in multipart messages' HTML ([#4462](https://github.com/deltachat/deltachat-core-rust/pull/4462)).
|
||||
- Ignore garbage at the end of the keys.
|
||||
|
||||
## [1.151.6] - 2024-12-11
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Don't add "Failed to send message to ..." info messages to group chats.
|
||||
- Add info messages about implicit membership changes if group member list is recreated ([#6314](https://github.com/deltachat/deltachat-core-rust/pull/6314)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add self-addition message to chat when recreating member list.
|
||||
- Do not subscribe to heartbeat if already subscribed via metadata.
|
||||
|
||||
### Build system
|
||||
|
||||
- Add idna 0.5.0 exception into deny.toml.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update links to Node.js bindings in the README.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Factor out `wait_for_all_work_done()`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Notifiy more prominently & in more tests about false positives when running `cargo test` ([#6308](https://github.com/deltachat/deltachat-core-rust/pull/6308)).
|
||||
|
||||
## [1.151.5] - 2024-12-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove dc_all_work_done().
|
||||
|
||||
### Security
|
||||
|
||||
- cargo: Update rPGP to 0.14.2.
|
||||
|
||||
This fixes [Panics on Malformed Untrusted Input](https://github.com/rpgp/rpgp/security/advisories/GHSA-9rmp-2568-59rv)
|
||||
and [Potential Resource Exhaustion when handling Untrusted Messages](https://github.com/rpgp/rpgp/security/advisories/GHSA-4grw-m28r-q285).
|
||||
This allows the attacker to crash the application via specially crafted messages and keys.
|
||||
We recommend all users and bot operators to upgrade to the latest version.
|
||||
There is no impact on the confidentiality of the messages and keys so no action other than upgrading is needed.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Store plaintext in mime_headers of truncated sent messages ([#6273](https://github.com/deltachat/deltachat-core-rust/pull/6273)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document `push` module.
|
||||
- Remove mention of non-existent `nightly` feature.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix panic in `receive_emails` benchmark ([#6306](https://github.com/deltachat/deltachat-core-rust/pull/6306)).
|
||||
|
||||
## [1.151.4] - 2024-12-03
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Encrypt notification tokens.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Replace connectivity state "Connected" with "Preparing".
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Beta clippy suggestions ([#6271](https://github.com/deltachat/deltachat-core-rust/pull/6271)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix `cargo check` for `receive_emails` benchmark.
|
||||
|
||||
### CI
|
||||
|
||||
- Also run cargo check without all-features.
|
||||
|
||||
## [1.151.3] - 2024-12-02
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Remove experimental `request_internet_access` option from webxdc's `manifest.toml`.
|
||||
- Add getWebxdcHref to json api ([#6281](https://github.com/deltachat/deltachat-core-rust/pull/6281)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.83.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update dc_msg_get_info_type() and dc_get_securejoin_qr() ([#6269](https://github.com/deltachat/deltachat-core-rust/pull/6269)).
|
||||
- Fix references to iroh-related headers in peer_channels docs.
|
||||
- Improve CFFI docs, link to corresponding JSON-RPC docs.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Allow the user to replace maps integration ([#5678](https://github.com/deltachat/deltachat-core-rust/pull/5678)).
|
||||
- Mark saved messages chat as protected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Close iroh endpoint when I/O is stopped.
|
||||
- Do not add protection messages to Saved Messages chat.
|
||||
- Mark Saved Messages chat as protected if it exists.
|
||||
- Sync chat action even if sync message arrives before first one from contact ([#6259](https://github.com/deltachat/deltachat-core-rust/pull/6259)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove some .unwrap() calls.
|
||||
- Create_status_update_record: Remove double check of info_msg_id.
|
||||
- Use Option::or_else() to dedup emitting IncomingWebxdcNotify.
|
||||
|
||||
## [1.151.2] - 2024-11-26
|
||||
|
||||
### API-Changes
|
||||
@@ -621,7 +372,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
- Reset quota on configured address change ([#5908](https://github.com/deltachat/deltachat-core-rust/pull/5908)).
|
||||
- Do not emit progress 1000 when configuration is cancelled.
|
||||
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/deltachat/deltachat-core-rust/pull/5338)).
|
||||
- Re-add tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
|
||||
- Readd tokens.foreign_id column ([#6038](https://github.com/deltachat/deltachat-core-rust/pull/6038)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
@@ -1212,7 +963,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: re-enable `log_cli`.
|
||||
- deltachat-rpc-client: reenable `log_cli`.
|
||||
|
||||
## [1.140.0] - 2024-06-04
|
||||
|
||||
@@ -2149,7 +1900,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
|
||||
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
|
||||
- deltachat-repl: Enable INFO logging by default and add timestamps.
|
||||
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elements based on the configuration key which is a part of the event.
|
||||
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
|
||||
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
|
||||
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
|
||||
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
|
||||
@@ -5618,13 +5369,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.151.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.150.0..v1.151.0
|
||||
[1.151.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.0..v1.151.1
|
||||
[1.151.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.1..v1.151.2
|
||||
[1.151.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.2..v1.151.3
|
||||
[1.151.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.3..v1.151.4
|
||||
[1.151.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.4..v1.151.5
|
||||
[1.151.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.5..v1.151.6
|
||||
[1.152.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.151.6..v1.152.0
|
||||
[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1
|
||||
[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2
|
||||
[1.153.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.2..v1.153.0
|
||||
[1.154.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.153.0..v1.154.0
|
||||
[1.154.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.0..v1.154.1
|
||||
|
||||
@@ -27,7 +27,7 @@ add_custom_command(
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ If you want to contribute a code, follow this guide.
|
||||
CI runs the tests and checks code formatting.
|
||||
|
||||
While it is running, self-review your PR to make sure all the changes you expect are there
|
||||
and there are no accidentally committed unrelated changes and files.
|
||||
and there are no accidentally commited unrelated changes and files.
|
||||
|
||||
Push the necessary fixup commits or force-push to your branch if needed.
|
||||
|
||||
|
||||
711
Cargo.lock
generated
711
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -39,11 +39,11 @@ format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "7", default-features=false, features = ["std"] }
|
||||
@@ -52,7 +52,7 @@ chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
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.10"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
@@ -62,10 +62,10 @@ http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.28.1", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
kamadak-exif = "0.6.0"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
@@ -76,7 +76,7 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.14.2", default-features = false }
|
||||
pgp = { version = "0.14.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -85,15 +85,15 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.10.1"
|
||||
rustls = { version = "0.23.20", default-features = false }
|
||||
rustls-pki-types = "1.10.0"
|
||||
rustls = { version = "0.23.14", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
@@ -101,15 +101,15 @@ tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-rustls = { version = "0.26.1", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-rustls = { version = "0.26.0", default-features = false }
|
||||
tokio-stream = { version = "0.1.16", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.7"
|
||||
webpki-roots = "0.26.6"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -120,7 +120,7 @@ nu-ansi-term = { workspace = true }
|
||||
pretty_assertions = "1.4.1"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = { workspace = true }
|
||||
testdir = "0.9.3"
|
||||
testdir = "0.9.0"
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace]
|
||||
@@ -150,7 +150,6 @@ harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "receive_emails"
|
||||
required-features = ["internals"]
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
@@ -169,12 +168,12 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.39", default-features = false }
|
||||
chrono = { version = "0.4.38", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.5.0"
|
||||
futures-lite = "2.4.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -186,10 +185,10 @@ rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.14.0"
|
||||
tempfile = "3.13.0"
|
||||
thiserror = "1"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.13"
|
||||
tokio-util = "0.7.11"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ $ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
||||
## Features
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
|
||||
## Update Provider Data
|
||||
|
||||
@@ -177,8 +178,8 @@ Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js**
|
||||
- over JSON-RPC: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||
- over CFFI[^1]: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat/)\]
|
||||
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go**
|
||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||
|
||||
@@ -12,18 +12,18 @@ use deltachat::{
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn recv_all_emails(context: Context, iteration: u32) -> Context {
|
||||
async fn recv_all_emails(context: Context) -> Context {
|
||||
for i in 0..100 {
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Mr.{iteration}.{i}@testrun.org
|
||||
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com
|
||||
From: sender@testrun.org
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
In-Reply-To: Mr.{iteration}.{i_dec}@testrun.org
|
||||
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
@@ -41,11 +41,11 @@ Hello {i}",
|
||||
|
||||
/// Receive 100 emails that remove charlie@example.com and add
|
||||
/// him back
|
||||
async fn recv_groupmembership_emails(context: Context, iteration: u32) -> Context {
|
||||
async fn recv_groupmembership_emails(context: Context) -> Context {
|
||||
for i in 0..50 {
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Gr.{iteration}.ADD.{i}@testrun.org
|
||||
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||
From: sender@testrun.org
|
||||
@@ -53,12 +53,13 @@ Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
Chat-Group-Member-Added: charlie@example.com
|
||||
In-Reply-To: Gr.{iteration}.REMOVE.{i_dec}@testrun.org
|
||||
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
@@ -67,7 +68,7 @@ Hello {i}",
|
||||
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Gr.{iteration}.REMOVE.{i}@testrun.org
|
||||
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||
From: sender@testrun.org
|
||||
@@ -75,12 +76,14 @@ Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
Chat-Group-Member-Removed: charlie@example.com
|
||||
In-Reply-To: Gr.{iteration}.ADD.{i}@testrun.org
|
||||
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}"
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
@@ -126,13 +129,11 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(create_context());
|
||||
let mut i = 0;
|
||||
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
i += 1;
|
||||
async move {
|
||||
recv_all_emails(black_box(ctx), i).await;
|
||||
recv_all_emails(black_box(ctx)).await;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -141,13 +142,11 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(create_context());
|
||||
let mut i = 0;
|
||||
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
i += 1;
|
||||
async move {
|
||||
recv_groupmembership_emails(black_box(ctx), i).await;
|
||||
recv_groupmembership_emails(black_box(ctx)).await;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
80
contrib/proxy.py
Normal file
80
contrib/proxy.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# Examples:
|
||||
#
|
||||
# Original server that doesn't use SSL:
|
||||
# ./proxy.py 8080 imap.nauta.cu 143
|
||||
# ./proxy.py 8081 smtp.nauta.cu 25
|
||||
#
|
||||
# Original server that uses SSL:
|
||||
# ./proxy.py 8080 testrun.org 993 --ssl
|
||||
# ./proxy.py 8081 testrun.org 465 --ssl
|
||||
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import selectors
|
||||
import ssl
|
||||
import socket
|
||||
import socketserver
|
||||
|
||||
|
||||
class Proxy(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
|
||||
self.real_host = real_host
|
||||
self.real_port = real_port
|
||||
self.use_ssl = use_ssl
|
||||
super().__init__((proxy_host, proxy_port), RequestHandler)
|
||||
|
||||
|
||||
class RequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
|
||||
|
||||
total = 0
|
||||
real_server = (self.server.real_host, self.server.real_port)
|
||||
with socket.create_connection(real_server) as sock:
|
||||
if self.server.use_ssl:
|
||||
context = ssl.create_default_context()
|
||||
sock = context.wrap_socket(
|
||||
sock, server_hostname=real_server[0])
|
||||
|
||||
forward = {self.request: sock, sock: self.request}
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(self.request, selectors.EVENT_READ,
|
||||
self.client_address)
|
||||
sel.register(sock, selectors.EVENT_READ, real_server)
|
||||
|
||||
active = True
|
||||
while active:
|
||||
events = sel.select()
|
||||
for key, mask in events:
|
||||
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
|
||||
data = key.fileobj.recv(1024)
|
||||
received = len(data)
|
||||
total += received
|
||||
print(data)
|
||||
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
|
||||
if data:
|
||||
forward[key.fileobj].sendall(data)
|
||||
else:
|
||||
print('\nCLOSING CONNECTION.\n\n')
|
||||
forward[key.fileobj].close()
|
||||
key.fileobj.close()
|
||||
active = False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser(description='Simple Python Proxy')
|
||||
p.add_argument(
|
||||
"proxy_port", help="the port where the proxy will listen", type=int)
|
||||
p.add_argument('host', help="the real host")
|
||||
p.add_argument('port', help="the port of the real host", type=int)
|
||||
p.add_argument("--ssl", help="use ssl to connect to the real host",
|
||||
action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
|
||||
proxy.serve_forever()
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -15,7 +15,7 @@ crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
deltachat = { workspace = true, default-features = false }
|
||||
deltachat-jsonrpc = { workspace = true }
|
||||
deltachat-jsonrpc = { workspace = true, optional = true }
|
||||
libc = { workspace = true }
|
||||
human-panic = { version = "2", default-features = false }
|
||||
num-traits = { workspace = true }
|
||||
@@ -30,4 +30,5 @@ yerpc = { workspace = true, features = ["anyhow_expose"] }
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
|
||||
jsonrpc = ["dep:deltachat-jsonrpc"]
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts
|
||||
* default=send and request read receipts, only send but not request if `bot` is set
|
||||
* default=send and request read receipts, only send but not reuqest if `bot` is set
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
@@ -722,6 +722,12 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
int dc_get_push_state (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Only used by the python tests.
|
||||
*/
|
||||
int dc_all_work_done (dc_context_t* context);
|
||||
|
||||
|
||||
// connect
|
||||
|
||||
/**
|
||||
@@ -963,6 +969,54 @@ uint32_t dc_create_chat_by_contact_id (dc_context_t* context, uint32_t co
|
||||
uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Prepare a message for sending.
|
||||
*
|
||||
* Call this function if the file to be sent is still in creation.
|
||||
* Once you're done with creating the file, call dc_send_msg() as usual
|
||||
* and the message will really be sent.
|
||||
*
|
||||
* This is useful as the user can already send the next messages while
|
||||
* e.g. the recoding of a video is not yet finished. Or the user can even forward
|
||||
* the message with the file being still in creation to other groups.
|
||||
*
|
||||
* Files being sent with the increation-method must be placed in the
|
||||
* blob directory, see dc_get_blobdir().
|
||||
* If the increation-method is not used - which is probably the normal case -
|
||||
* dc_send_msg() copies the file to the blob directory if it is not yet there.
|
||||
* To distinguish the two cases, msg->state must be set properly. The easiest
|
||||
* way to ensure this is to re-use the same object for both calls.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* char* blobdir = dc_get_blobdir(context);
|
||||
* char* file_to_send = mprintf("%s/%s", blobdir, "send.mp4")
|
||||
*
|
||||
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_VIDEO);
|
||||
* dc_msg_set_file(msg, file_to_send, NULL);
|
||||
* dc_prepare_msg(context, chat_id, msg);
|
||||
*
|
||||
* // ... create the file ...
|
||||
*
|
||||
* dc_send_msg(context, chat_id, msg);
|
||||
*
|
||||
* dc_msg_unref(msg);
|
||||
* free(file_to_send);
|
||||
* dc_str_unref(file_to_send);
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID to send the message to.
|
||||
* @param msg The message object to send to the chat defined by the chat ID.
|
||||
* On success, msg_id and state of the object are set up,
|
||||
* The function does not take ownership of the object,
|
||||
* so you have to free it using dc_msg_unref() as usual.
|
||||
* @return The ID of the message that is being prepared.
|
||||
*/
|
||||
uint32_t dc_prepare_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Send a message defined by a dc_msg_t object to a chat.
|
||||
*
|
||||
@@ -987,11 +1041,13 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
|
||||
* If that fails, is not possible, or the image is already small enough, the image is sent as original.
|
||||
* If you want images to be always sent as the original file, use the #DC_MSG_FILE type.
|
||||
*
|
||||
* Videos and other file types are currently not recoded by the library.
|
||||
* Videos and other file types are currently not recoded by the library,
|
||||
* with dc_prepare_msg(), however, you can do that from the UI.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID to send the message to.
|
||||
* If dc_prepare_msg() was called before, this parameter can be 0.
|
||||
* @param msg The message object to send to the chat defined by the chat ID.
|
||||
* On success, msg_id of the object is set up,
|
||||
* The function does not take ownership of the object,
|
||||
@@ -1008,6 +1064,7 @@ uint32_t dc_send_msg (dc_context_t* context, uint32_t ch
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID to send the message to.
|
||||
* If dc_prepare_msg() was called before, this parameter can be 0.
|
||||
* @param msg The message object to send to the chat defined by the chat ID.
|
||||
* On success, msg_id of the object is set up,
|
||||
* The function does not take ownership of the object,
|
||||
@@ -3934,7 +3991,7 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
|
||||
*
|
||||
* Outgoing message states:
|
||||
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING.
|
||||
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
|
||||
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
|
||||
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
|
||||
@@ -4144,13 +4201,14 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
* - internet_access:
|
||||
* true if the Webxdc should get internet access;
|
||||
* this is the case i.e. for experimental maps integration.
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* - self_addr: address to be used for `window.webxdc.selfAddr` in JS land.
|
||||
* - send_update_interval: Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||
* Should be exposed to `webxdc.sendUpdateInterval` in JS land.
|
||||
* - send_update_max_size: Maximum number of bytes accepted for a serialized update object.
|
||||
* Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
|
||||
+ Should be exposed to `webxdc.sendUpdateMaxSize` in JS land.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4484,6 +4542,20 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
*/
|
||||
char* dc_msg_get_webxdc_href (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Check if a message is still in creation. A message is in creation between
|
||||
* the calls to dc_prepare_msg() and dc_send_msg().
|
||||
*
|
||||
* Typically, this is used for videos that are recoded by the UI before
|
||||
* they can be sent.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=message is still in creation (dc_send_msg() was not called yet),
|
||||
* 0=message no longer in creation.
|
||||
*/
|
||||
int dc_msg_is_increation (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the message is an Autocrypt Setup Message.
|
||||
@@ -4635,7 +4707,7 @@ int dc_msg_has_html (dc_msg_t* msg);
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
*
|
||||
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any further download action.
|
||||
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
|
||||
* It was fully downloaded, but we failed to decrypt it.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
*
|
||||
@@ -5393,8 +5465,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
/**
|
||||
* Message containing a sticker, similar to image.
|
||||
* NB: When sending, the message viewtype may be changed to `Image` by some heuristics like checking
|
||||
* for transparent pixels.
|
||||
* If possible, the UI should display the image without borders in a transparent way.
|
||||
* A click on a sticker will offer to install the sticker set in some future.
|
||||
*/
|
||||
@@ -5499,8 +5569,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
/**
|
||||
* Outgoing message being prepared. See dc_msg_get_state() for details.
|
||||
*
|
||||
* @deprecated 2024-12-07
|
||||
*/
|
||||
#define DC_STATE_OUT_PREPARING 18
|
||||
|
||||
@@ -5721,23 +5789,6 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
|
||||
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
|
||||
*
|
||||
* An overview of JSON-RPC calls is available at
|
||||
* <https://js.jsonrpc.delta.chat/classes/RawClient.html>.
|
||||
* Note that the page describes only the rough methods.
|
||||
* Calling convention, casing etc. does vary, this is a known flaw,
|
||||
* and at some point we will get to improve that :)
|
||||
*
|
||||
* Also, note that most calls are more high-level than this CFFI, require more database calls and are slower.
|
||||
* They're more suitable for an environment that is totally async and/or cannot use CFFI, which might not be true for native apps.
|
||||
*
|
||||
* Notable exceptions that exist only as JSON-RPC and probably never get a CFFI counterpart:
|
||||
* - getMessageReactions(), sendReaction()
|
||||
* - getHttpResponse()
|
||||
* - draftSelfReport()
|
||||
* - getAccountFileSize()
|
||||
* - importVcard(), parseVcard(), makeVcard()
|
||||
* - sendWebxdcRealtimeData, sendWebxdcRealtimeAdvertisement(), leaveWebxdcRealtime()
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param request JSON-RPC request as string
|
||||
@@ -5758,8 +5809,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
/**
|
||||
* Make a JSON-RPC call and return a response.
|
||||
*
|
||||
* See dc_jsonrpc_request() for an overview of possible calls and for more information.
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param input JSON-RPC request.
|
||||
@@ -6850,7 +6899,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Failed to send message to %1$s."
|
||||
///
|
||||
/// Unused. Was used in group chat status messages.
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the contact the message cannot be sent to
|
||||
#define DC_STR_FAILED_SENDING_TO 74
|
||||
|
||||
|
||||
@@ -35,8 +35,6 @@ use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
@@ -415,6 +413,16 @@ pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc
|
||||
block_on(ctx.push_state()) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_all_work_done()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move { ctx.all_work_done().await as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
context: *mut dc_context_t,
|
||||
@@ -978,6 +986,27 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_prepare_msg(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
msg: *mut dc_msg_t,
|
||||
) -> u32 {
|
||||
if context.is_null() || chat_id == 0 || msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_prepare_msg()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &mut *context;
|
||||
let ffi_msg: &mut MessageWrapper = &mut *msg;
|
||||
|
||||
block_on(async move {
|
||||
chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed to prepare message")
|
||||
})
|
||||
.to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_msg(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3694,6 +3723,16 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc
|
||||
ffi_msg.message.get_webxdc_href().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_is_increation()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.is_increation().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
@@ -4932,97 +4971,105 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
}
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
use super::*;
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
Box::into_raw(Box::new(instance))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(jsonrpc_instance));
|
||||
}
|
||||
|
||||
fn spawn_handle_jsonrpc_request(handle: RpcSession<CommandApi>, request: String) {
|
||||
spawn(async move {
|
||||
handle.handle_incoming(&request).await;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_request(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
request: *const libc::c_char,
|
||||
) {
|
||||
if jsonrpc_instance.is_null() || request.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_request()");
|
||||
return;
|
||||
}
|
||||
|
||||
let handle = &(*jsonrpc_instance).handle;
|
||||
let request = to_string_lossy(request);
|
||||
spawn_handle_jsonrpc_request(handle.clone(), request);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_next_response(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
block_on(api.receiver.recv())
|
||||
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
input: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
match res {
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let account_manager = &*account_manager;
|
||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||
account_manager.inner.clone(),
|
||||
));
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
Box::into_raw(Box::new(instance))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(jsonrpc_instance));
|
||||
}
|
||||
|
||||
fn spawn_handle_jsonrpc_request(handle: RpcSession<CommandApi>, request: String) {
|
||||
spawn(async move {
|
||||
handle.handle_incoming(&request).await;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_request(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
request: *const libc::c_char,
|
||||
) {
|
||||
if jsonrpc_instance.is_null() || request.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_request()");
|
||||
return;
|
||||
}
|
||||
|
||||
let handle = &(*jsonrpc_instance).handle;
|
||||
let request = to_string_lossy(request);
|
||||
spawn_handle_jsonrpc_request(handle.clone(), request);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_next_response(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
block_on(api.receiver.recv())
|
||||
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
input: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
match res {
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -33,7 +33,7 @@ base64 = { workspace = true }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.11.6", optional = true }
|
||||
env_logger = { version = "0.11.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||
|
||||
@@ -1767,7 +1767,7 @@ impl CommandApi {
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
update_str: String,
|
||||
_descr: Option<String>,
|
||||
_descr: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str)
|
||||
@@ -1829,18 +1829,6 @@ impl CommandApi {
|
||||
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
|
||||
}
|
||||
|
||||
/// Get href from a WebxdcInfoMessage which might include a hash holding
|
||||
/// information about a specific position or state in a webxdc app (optional)
|
||||
async fn get_webxdc_href(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
Ok(message.get_webxdc_href())
|
||||
}
|
||||
|
||||
/// Get blob encoded as base64 from a webxdc message
|
||||
///
|
||||
/// path is the path of the file within webxdc archive
|
||||
|
||||
@@ -69,7 +69,7 @@ pub enum EventType {
|
||||
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a message box then.
|
||||
/// in a messasge box then.
|
||||
Error { msg: String },
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
@@ -109,7 +109,6 @@ pub enum EventType {
|
||||
/// Incoming webxdc info or summary update, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingWebxdcNotify {
|
||||
chat_id: u32,
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
text: String,
|
||||
@@ -344,13 +343,11 @@ impl From<CoreEventType> for EventType {
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingWebxdcNotify {
|
||||
chat_id,
|
||||
contact_id,
|
||||
msg_id,
|
||||
text,
|
||||
href,
|
||||
} => IncomingWebxdcNotify {
|
||||
chat_id: chat_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
text,
|
||||
|
||||
@@ -85,8 +85,6 @@ pub struct MessageObject {
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
@@ -243,10 +241,6 @@ impl MessageObject {
|
||||
file_name: message.get_filename(),
|
||||
webxdc_info,
|
||||
|
||||
// On a WebxdcInfoMessage this might include a hash holding
|
||||
// information about a specific position or state in a webxdc app
|
||||
webxdc_href: message.get_webxdc_href(),
|
||||
|
||||
download_state,
|
||||
|
||||
reactions,
|
||||
@@ -273,9 +267,6 @@ pub enum MessageViewtype {
|
||||
Gif,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
Sticker,
|
||||
|
||||
@@ -57,7 +57,6 @@ impl WebxdcMessageInfo {
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
request_integration: _,
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval,
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.154.1"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -13,6 +13,7 @@ classifiers = [
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
@@ -23,7 +24,6 @@ classifiers = [
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
deltachat_rpc_client = [
|
||||
|
||||
@@ -41,7 +41,6 @@ class EventType(str, Enum):
|
||||
REACTIONS_CHANGED = "ReactionsChanged"
|
||||
INCOMING_MSG = "IncomingMsg"
|
||||
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
|
||||
INCOMING_REACTION = "IncomingReaction"
|
||||
MSGS_NOTICED = "MsgsNoticed"
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
|
||||
@@ -131,7 +131,10 @@ class Rpc:
|
||||
|
||||
def reader_loop(self) -> None:
|
||||
try:
|
||||
while line := self.process.stdout.readline():
|
||||
while True:
|
||||
line = self.process.stdout.readline()
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
@@ -147,7 +150,10 @@ class Rpc:
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
try:
|
||||
while request := self.request_queue.get():
|
||||
while True:
|
||||
request = self.request_queue.get()
|
||||
if not request:
|
||||
break
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
@@ -73,25 +73,22 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Alice deletes "vg-request".
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
for ac in [alice, bob]:
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
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 == protect
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -65,13 +65,13 @@ so by default it uses the prebuilds.
|
||||
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
|
||||
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
|
||||
|
||||
## How to build a version you can use locally on your host machine for development
|
||||
## How to build a version you can use localy on your host machine for development
|
||||
|
||||
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have separate scripts for making it work for local installation.
|
||||
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
|
||||
|
||||
- If you just need your host platform run `python scripts/make_local_dev_version.py`
|
||||
- note: this clears the `platform_package` folder
|
||||
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple platforms with `build_platform_package.py`
|
||||
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
|
||||
|
||||
## Thanks to nlnet
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.154.1"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
if (process.cwd() !== expected_cwd) {
|
||||
console.error(
|
||||
"CWD mismatch: this script needs to be run from " + expected_cwd,
|
||||
"CWD missmatch: this script needs to be run from " + expected_cwd,
|
||||
{ actual: process.cwd(), expected: expected_cwd }
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -40,7 +40,7 @@ const platform_package_names = await Promise.all(
|
||||
"has a different version than the version of the rpc server.",
|
||||
{ rpc_server: version, platform_package: p.version }
|
||||
);
|
||||
throw new Error("version mismatch");
|
||||
throw new Error("version missmatch");
|
||||
}
|
||||
return { folder_name: name, package_name: p.name };
|
||||
})
|
||||
|
||||
@@ -66,7 +66,7 @@ async fn main_impl() -> Result<()> {
|
||||
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||
// and go to stderr to avoid interferring with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
|
||||
12
deny.toml
12
deny.toml
@@ -18,9 +18,6 @@ ignore = [
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# idna 0.5.0
|
||||
"RUSTSEC-2024-0421",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -40,8 +37,10 @@ skip = [
|
||||
{ name = "futures-lite", version = "1.13.0" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "idna", version = "0.5.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "num_enum_derive", version = "0.5.11" },
|
||||
{ name = "num_enum", version = "0.5.11" },
|
||||
{ name = "proc-macro-crate", version = "1.3.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -51,9 +50,8 @@ skip = [
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.19.15" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
@@ -66,6 +64,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
]
|
||||
|
||||
@@ -82,7 +81,6 @@ allow = [
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
62
flake.lock
generated
62
flake.lock
generated
@@ -47,17 +47,16 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711088506,
|
||||
"narHash": "sha256-USdlY7Tx2oJWqFBpp10+03+h7eVhpkQ4s9t1ERjeIJE=",
|
||||
"lastModified": 1731393059,
|
||||
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -115,25 +114,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"new-fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"rust-analyzer-src": "rust-analyzer-src_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1734417396,
|
||||
"narHash": "sha256-32x1Z+Pz3Jv0cK9EG56cFTKXy/mZ/c+Ikxw+aVfKHp4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a18d41b26e998e95a598858fdb86ba22fb5da47d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1730207686,
|
||||
@@ -194,22 +174,6 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1734119587,
|
||||
"narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
@@ -231,9 +195,8 @@
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk",
|
||||
"new-fenix": "new-fenix",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_5"
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
@@ -253,23 +216,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1734386068,
|
||||
"narHash": "sha256-Py025JiD9lcPmldB7X1AEjq3WBTS60jZUJRtTDonmaE=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0a706f7d2ac093985eae317781200689cfd48b78",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
|
||||
13
flake.nix
13
flake.nix
@@ -1,19 +1,14 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
# Old Rust to build releases.
|
||||
fenix.url = "github:nix-community/fenix?rev=85f4139f3c092cf4afd9f9906d7ed218ef262c97";
|
||||
|
||||
# New Rust for development shell.
|
||||
new-fenix.url = "github:nix-community/fenix";
|
||||
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
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, new-fenix, android }:
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
@@ -544,13 +539,13 @@
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ new-fenix.overlays.default ];
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(new-fenix.packages.${system}.complete.withComponents [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
|
||||
270
fuzz/Cargo.lock
generated
270
fuzz/Cargo.lock
generated
@@ -146,7 +146,6 @@ dependencies = [
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -179,7 +178,7 @@ dependencies = [
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
]
|
||||
|
||||
@@ -191,7 +190,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
"synstructure 0.13.1",
|
||||
]
|
||||
|
||||
@@ -203,7 +202,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -275,7 +274,7 @@ dependencies = [
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
"stop-token",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -286,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -299,22 +298,23 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.10.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee04bcf0a7ebf5594f9aff84935dc8cb0490b65055913a7a4c4d08f81e181d6"
|
||||
checksum = "8709c0d4432be428a88a06746689a9cb543e8e27ef7f61ca4d0455003a3d8c5b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.13.1",
|
||||
"futures",
|
||||
"hostname",
|
||||
"log",
|
||||
"nom",
|
||||
"pin-project",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -326,7 +326,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -339,7 +339,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite 2.5.0",
|
||||
"pin-project",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -1040,7 +1040,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1108,7 +1108,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1119,7 +1119,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1152,7 +1152,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.151.5"
|
||||
version = "1.150.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1220,7 +1220,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tagger",
|
||||
"textwrap",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-io-timeout",
|
||||
"tokio-rustls",
|
||||
@@ -1263,7 +1263,7 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1300,7 +1300,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1331,7 +1331,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1341,7 +1341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1361,7 +1361,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1579,7 +1579,7 @@ dependencies = [
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1670,7 +1670,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1690,7 +1690,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1814,7 +1814,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"log",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
@@ -2061,7 +2061,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2331,7 +2331,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
@@ -2355,7 +2355,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2718,7 +2718,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"ssh-key",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"ttl_cache",
|
||||
"url",
|
||||
"zeroize",
|
||||
@@ -2828,7 +2828,7 @@ dependencies = [
|
||||
"netlink-packet-route",
|
||||
"netlink-sys",
|
||||
"netwatch",
|
||||
"num_enum",
|
||||
"num_enum 0.7.2",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project",
|
||||
@@ -2848,7 +2848,7 @@ dependencies = [
|
||||
"strum",
|
||||
"stun-rs",
|
||||
"surge-ping",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@@ -2880,7 +2880,7 @@ dependencies = [
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls",
|
||||
"socket2 0.5.6",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2898,7 +2898,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"slab",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2954,7 +2954,7 @@ dependencies = [
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -3186,7 +3186,7 @@ dependencies = [
|
||||
"serde_bencode",
|
||||
"serde_bytes",
|
||||
"sha1_smol",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3349,7 +3349,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"paste",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3363,7 +3363,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -3401,7 +3401,7 @@ dependencies = [
|
||||
"rtnetlink",
|
||||
"serde",
|
||||
"socket2 0.5.6",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3500,7 +3500,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3543,13 +3543,34 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
|
||||
dependencies = [
|
||||
"num_enum_derive 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"num_enum_derive 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3558,10 +3579,10 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3792,7 +3813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -3816,7 +3837,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3832,15 +3853,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pgp"
|
||||
version = "0.14.2"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9"
|
||||
checksum = "49bb5f77aaf8ae1ed6fe63387ad513b10cd44716fd053ecc227b9493c096cdb2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"aes-kw",
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
"base64 0.21.7",
|
||||
"bitfield",
|
||||
"block-padding",
|
||||
"blowfish",
|
||||
@@ -3876,7 +3897,7 @@ dependencies = [
|
||||
"nom",
|
||||
"num-bigint-dig",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"num_enum 0.5.11",
|
||||
"ocb3",
|
||||
"p256",
|
||||
"p384",
|
||||
@@ -3890,7 +3911,7 @@ dependencies = [
|
||||
"sha3",
|
||||
"signature",
|
||||
"smallvec",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"twofish",
|
||||
"x25519-dalek",
|
||||
"x448",
|
||||
@@ -3914,7 +3935,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3946,7 +3967,7 @@ dependencies = [
|
||||
"mainline",
|
||||
"self_cell",
|
||||
"simple-dns",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"ureq",
|
||||
"wasm-bindgen",
|
||||
@@ -4000,7 +4021,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4081,12 +4102,12 @@ dependencies = [
|
||||
"iroh-metrics",
|
||||
"libc",
|
||||
"netwatch",
|
||||
"num_enum",
|
||||
"num_enum 0.7.2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"socket2 0.5.6",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -4178,13 +4199,23 @@ dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4221,9 +4252,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -4248,7 +4279,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4305,29 +4336,26 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustls",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.9"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
|
||||
checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.2.11",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4640,21 +4668,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.7"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
|
||||
checksum = "72f1471dbb4be5de45050e8ef7040625298ccb9efe941419ac2697088715925f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"const-oid",
|
||||
"digest",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4673,7 +4702,7 @@ dependencies = [
|
||||
"netlink-proto",
|
||||
"netlink-sys",
|
||||
"nix",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -4762,9 +4791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.19"
|
||||
version = "0.23.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
|
||||
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -4803,9 +4832,6 @@ name = "rustls-pki-types"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
@@ -5024,7 +5050,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5099,7 +5125,6 @@ checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"sha1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5157,7 +5182,7 @@ dependencies = [
|
||||
"shadowsocks-crypto",
|
||||
"socket2 0.5.6",
|
||||
"spin 0.9.8",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
"url",
|
||||
@@ -5303,9 +5328,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
@@ -5390,7 +5415,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"struct_iterable_internal",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5418,7 +5443,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5462,7 +5487,7 @@ dependencies = [
|
||||
"pnet_packet",
|
||||
"rand 0.8.5",
|
||||
"socket2 0.5.6",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -5480,9 +5505,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
version = "2.0.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5526,7 +5551,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5601,16 +5626,7 @@ version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.6",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5621,18 +5637,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5738,7 +5743,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5818,7 +5823,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"js-sys",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"wasm-bindgen",
|
||||
@@ -5850,7 +5855,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
"toml_edit 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5862,6 +5867,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
@@ -5901,7 +5917,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5972,7 +5988,7 @@ dependencies = [
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
@@ -6201,7 +6217,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -6235,7 +6251,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -6255,7 +6271,7 @@ dependencies = [
|
||||
"event-listener 4.0.3",
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6268,16 +6284,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.7"
|
||||
@@ -6387,7 +6393,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6398,7 +6404,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6687,7 +6693,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
@@ -6736,7 +6742,7 @@ dependencies = [
|
||||
"nom",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 1.0.58",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
]
|
||||
|
||||
@@ -6796,7 +6802,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -299,6 +299,10 @@ export class Message {
|
||||
return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
|
||||
}
|
||||
|
||||
isIncreation() {
|
||||
return Boolean(binding.dcn_msg_is_increation(this.dc_msg))
|
||||
}
|
||||
|
||||
isInfo() {
|
||||
return Boolean(binding.dcn_msg_is_info(this.dc_msg))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const buildArgs = [
|
||||
'build',
|
||||
'--release',
|
||||
'--features',
|
||||
'vendored',
|
||||
'vendored,jsonrpc',
|
||||
'-p',
|
||||
'deltachat_ffi'
|
||||
]
|
||||
|
||||
@@ -2374,6 +2374,17 @@ NAPI_METHOD(dcn_msg_is_forwarded) {
|
||||
NAPI_RETURN_INT32(is_forwarded);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_msg_is_increation) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_MSG();
|
||||
|
||||
//TRACE("calling..");
|
||||
int is_increation = dc_msg_is_increation(dc_msg);
|
||||
//TRACE("result %d", is_increation);
|
||||
|
||||
NAPI_RETURN_INT32(is_increation);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_msg_is_info) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_MSG();
|
||||
@@ -3544,6 +3555,7 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_has_location);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_has_html);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_is_forwarded);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_is_increation);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_is_info);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_is_sent);
|
||||
NAPI_EXPORT_FUNCTION(dcn_msg_is_setupmessage);
|
||||
|
||||
@@ -536,6 +536,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
strictEqual(msg.getWidth(), 0, 'no message width')
|
||||
strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
|
||||
strictEqual(msg.isForwarded(), false, 'not forwarded')
|
||||
strictEqual(msg.isIncreation(), false, 'not in creation')
|
||||
strictEqual(msg.isInfo(), false, 'not an info message')
|
||||
strictEqual(msg.isSent(), false, 'messge is not sent')
|
||||
strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.154.1"
|
||||
"version": "1.151.2"
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ python3-venv` should give you a usable python installation.
|
||||
|
||||
First, build the core library::
|
||||
|
||||
cargo build --release -p deltachat_ffi
|
||||
cargo build --release -p deltachat_ffi --features jsonrpc
|
||||
|
||||
`jsonrpc` feature is required even if not used by the bindings
|
||||
because `deltachat.h` includes JSON-RPC functions unconditionally.
|
||||
|
||||
Create the virtual environment and activate it::
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.154.1"
|
||||
version = "1.151.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.7"
|
||||
authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
|
||||
@@ -671,6 +671,9 @@ class Account:
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
"""start this account's IO scheduling (Rust-core async scheduler).
|
||||
|
||||
|
||||
@@ -271,7 +271,8 @@ class Chat:
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
previously returned by
|
||||
e.g. :meth:`deltachat.message.Message.new_empty`.
|
||||
e.g. :meth:`deltachat.message.Message.new_empty` or
|
||||
:meth:`prepare_file`.
|
||||
:raises ValueError: if message can not be sent.
|
||||
|
||||
:returns: a :class:`deltachat.message.Message` instance as
|
||||
@@ -340,6 +341,37 @@ class Chat:
|
||||
raise ValueError("message could not be sent")
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def prepare_message(self, msg):
|
||||
"""prepare a message for sending.
|
||||
|
||||
:param msg: the message to be prepared.
|
||||
:returns: a :class:`deltachat.message.Message` instance.
|
||||
This is the same object that was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be prepared")
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
|
||||
return msg
|
||||
|
||||
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
||||
"""prepare a message for sending and return the resulting Message instance.
|
||||
|
||||
To actually send the message, call :meth:`send_prepared`.
|
||||
The file must be inside the blob directory.
|
||||
|
||||
:param path: path to the file.
|
||||
:param mime_type: the mime-type of this file, defaults to auto-detection.
|
||||
:param view_type: "text", "image", "gif", "audio", "video", "file"
|
||||
:raises ValueError: if message can not be prepared/chat does not exist.
|
||||
:returns: the resulting :class:`Message` instance
|
||||
"""
|
||||
msg = Message.new_empty(self.account, view_type)
|
||||
msg.set_file(path, mime_type)
|
||||
return self.prepare_message(msg)
|
||||
|
||||
def send_prepared(self, message):
|
||||
"""send a previously prepared message.
|
||||
|
||||
|
||||
@@ -158,6 +158,12 @@ class FFIEventTracker:
|
||||
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def wait_for_all_work_done(self):
|
||||
while True:
|
||||
if self.account.all_work_done():
|
||||
return
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile(f"(?:{event_name_regex}).*")
|
||||
|
||||
@@ -152,7 +152,7 @@ class TestProcess:
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
"""provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can reuse already known live configs.
|
||||
so that test functions can re-use already known live configs.
|
||||
"""
|
||||
chatmail_opt = self.pytestconfig.getoption("--chatmail")
|
||||
if chatmail_opt:
|
||||
|
||||
@@ -1253,10 +1253,7 @@ def test_no_old_msg_is_fresh(acfactory, lp):
|
||||
|
||||
def test_prefer_encrypt(acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "1")
|
||||
ac3.set_config("e2ee_enabled", "0")
|
||||
@@ -1279,8 +1276,7 @@ def test_prefer_encrypt(acfactory, lp):
|
||||
lp.sec("ac2: sending message to ac1")
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
msg2 = chat2.send_text("message2")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg2.is_encrypted()
|
||||
assert not msg2.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending message to group chat with ac2 and ac3")
|
||||
@@ -1296,8 +1292,8 @@ def test_prefer_encrypt(acfactory, lp):
|
||||
ac3.set_config("e2ee_enabled", "1")
|
||||
chat3 = ac3.create_chat(ac1)
|
||||
msg4 = chat3.send_text("message4")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg4.is_encrypted()
|
||||
# ac1 still does not prefer encryption
|
||||
assert not msg4.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
|
||||
@@ -1370,9 +1366,10 @@ def test_quote_encrypted(acfactory, lp):
|
||||
msg_draft.quote = quoted_msg
|
||||
chat.set_draft(msg_draft)
|
||||
|
||||
# Get the draft and send it.
|
||||
# Get the draft, prepare and send it.
|
||||
msg_draft = chat.get_draft()
|
||||
chat.send_msg(msg_draft)
|
||||
msg_out = chat.prepare_message(msg_draft)
|
||||
chat.send_prepared(msg_out)
|
||||
|
||||
chat.set_draft(None)
|
||||
assert chat.get_draft() is None
|
||||
@@ -1902,11 +1899,10 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `DC_CONNECTIVITY_CONNECTED`, "
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
"all messages are fetched",
|
||||
)
|
||||
|
||||
@@ -1915,7 +1911,7 @@ def test_connectivity(acfactory, lp):
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
idle1.wait_for_new_message()
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_all_work_done()
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
@@ -1931,6 +1927,30 @@ def test_connectivity(acfactory, lp):
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
|
||||
|
||||
ac1.maybe_network()
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
|
||||
ac1.create_contact(ac2).block()
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
idle1.wait_for_new_message()
|
||||
ac1.maybe_network()
|
||||
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
|
||||
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
@@ -1941,6 +1961,32 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
|
||||
def test_all_work_done(acfactory, lp):
|
||||
"""
|
||||
Tests that calling start_io() immediately followed by maybe_network()
|
||||
and then waiting for all_work_done() reliably fetches the messages
|
||||
delivered while account was offline.
|
||||
In other words, connectivity should not change to a state
|
||||
where all_work_done() returns true until IMAP connection goes idle.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
ac2.create_chat(ac1).send_text("Hi")
|
||||
idle1.wait_for_new_message()
|
||||
|
||||
ac1.start_io()
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_all_work_done()
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
|
||||
hundreds of times, because uid_next was not updated.
|
||||
@@ -2294,8 +2340,9 @@ def test_group_quote(acfactory, lp):
|
||||
reply_msg = Message.new_empty(msg.chat.account, "text")
|
||||
reply_msg.set_text("reply")
|
||||
reply_msg.quote = msg
|
||||
reply_msg = msg.chat.prepare_message(reply_msg)
|
||||
assert reply_msg.quoted_text == "hello"
|
||||
msg.chat.send_msg(reply_msg)
|
||||
msg.chat.send_prepared(reply_msg)
|
||||
|
||||
lp.sec("ac3: receiving reply")
|
||||
received_reply = ac3._evtracker.wait_next_incoming_message()
|
||||
|
||||
107
python/tests/test_2_increation.py
Normal file
107
python/tests/test_2_increation.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import os.path
|
||||
import shutil
|
||||
from filecmp import cmp
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def wait_msg_delivered(account, msg_list):
|
||||
"""wait for one or more MSG_DELIVERED events to match msg_list contents."""
|
||||
msg_list = list(msg_list)
|
||||
while msg_list:
|
||||
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
msg_list.remove((ev.data1, ev.data2))
|
||||
|
||||
|
||||
def wait_msgs_changed(account, msgs_list):
|
||||
"""wait for one or more MSGS_CHANGED events to match msgs_list contents."""
|
||||
account.log(f"waiting for msgs_list={msgs_list}")
|
||||
msgs_list = list(msgs_list)
|
||||
while msgs_list:
|
||||
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
for i, (data1, data2) in enumerate(msgs_list):
|
||||
if ev.data1 == data1:
|
||||
if data2 is None or ev.data2 == data2:
|
||||
del msgs_list[i]
|
||||
break
|
||||
else:
|
||||
account.log(f"waiting mismatch data1={data1} data2={data2}")
|
||||
return ev.data2
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.touch()
|
||||
with pytest.raises(Exception):
|
||||
chat.prepare_message_file(str(src))
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.write_text("hello there\n")
|
||||
msg = chat.send_file(str(src))
|
||||
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
||||
assert msg.filename.endswith(".txt")
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
|
||||
|
||||
lp.sec("create a message with a file in creation")
|
||||
orig = data.get_path("d.png")
|
||||
path = os.path.join(ac1.get_blobdir(), "d.png")
|
||||
with open(path, "x") as fp:
|
||||
fp.write("preparing")
|
||||
prepared_original = chat.prepare_message_file(path)
|
||||
assert prepared_original.is_out_preparing()
|
||||
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
|
||||
|
||||
lp.sec("create a new group")
|
||||
chat2 = ac1.create_group_chat("newgroup")
|
||||
wait_msgs_changed(ac1, [(0, 0)])
|
||||
|
||||
lp.sec("add a contact to new group")
|
||||
chat2.add_contact(ac2)
|
||||
wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
|
||||
lp.sec("forward the message while still in creation")
|
||||
ac1.forward_messages([prepared_original], chat2)
|
||||
forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||
assert forwarded_msg.is_out_preparing()
|
||||
|
||||
lp.sec("finish creating the file and send it")
|
||||
assert prepared_original.is_out_preparing()
|
||||
shutil.copyfile(orig, path)
|
||||
chat.send_prepared(prepared_original)
|
||||
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
|
||||
|
||||
lp.sec("check that both forwarded and original message are proper.")
|
||||
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
|
||||
|
||||
fwd_msg = ac1.get_message_by_id(forwarded_id)
|
||||
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
||||
|
||||
lp.sec("wait for both messages to be delivered to SMTP")
|
||||
wait_msg_delivered(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
|
||||
|
||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||
received_original = ac2._evtracker.wait_next_incoming_message()
|
||||
assert cmp(received_original.filename, orig, shallow=False)
|
||||
|
||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||
received_copy = ac2._evtracker.wait_next_incoming_message()
|
||||
assert received_copy.id != received_original.id
|
||||
assert cmp(received_copy.filename, orig, shallow=False)
|
||||
@@ -378,6 +378,30 @@ class TestOfflineChat:
|
||||
with pytest.raises(ValueError):
|
||||
chat1.send_text("msg1")
|
||||
|
||||
def test_prepare_message_and_send(self, ac1, chat1):
|
||||
msg = chat1.prepare_message(Message.new_empty(chat1.account, "text"))
|
||||
msg.set_text("hello world")
|
||||
assert msg.text == "hello world"
|
||||
assert msg.id > 0
|
||||
chat1.send_prepared(msg)
|
||||
assert "Sent" in msg.get_message_info()
|
||||
str(msg)
|
||||
repr(msg)
|
||||
assert msg == ac1.get_message_by_id(msg.id)
|
||||
|
||||
def test_prepare_file(self, ac1, chat1):
|
||||
blobdir = ac1.get_blobdir()
|
||||
p = os.path.join(blobdir, "somedata.txt")
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
message = chat1.prepare_message_file(p)
|
||||
assert message.id > 0
|
||||
message.set_text("hello world")
|
||||
assert message.is_out_preparing()
|
||||
assert message.text == "hello world"
|
||||
chat1.send_prepared(message)
|
||||
assert "Sent" in message.get_message_info()
|
||||
|
||||
def test_message_eq_contains(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
msg2 = None
|
||||
@@ -667,7 +691,8 @@ class TestOfflineChat:
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg1 = chat1.prepare_message(msg)
|
||||
msg1.set_text("hello")
|
||||
chat1.set_draft(msg1)
|
||||
msg1.set_text("obsolete")
|
||||
@@ -686,6 +711,21 @@ class TestOfflineChat:
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
def test_quote(self, chat1):
|
||||
"""Offline quoting test"""
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg.set_text("Multi\nline\nmessage")
|
||||
assert msg.quoted_text is None
|
||||
|
||||
# Prepare message to assign it a Message-Id.
|
||||
# Messages without Message-Id cannot be quoted.
|
||||
msg = chat1.prepare_message(msg)
|
||||
|
||||
reply_msg = Message.new_empty(chat1.account, "text")
|
||||
reply_msg.set_text("reply")
|
||||
reply_msg.quote = msg
|
||||
assert reply_msg.quoted_text == "Multi\nline\nmessage"
|
||||
|
||||
def test_group_chat_many_members_add_remove(self, ac1, lp):
|
||||
lp.sec("ac1: creating group chat with 10 other members")
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
|
||||
@@ -212,13 +212,8 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
0 / 0
|
||||
|
||||
cap = Queue()
|
||||
|
||||
# Make sure the next attempt to log an event fails.
|
||||
ac1.add_account_plugin(FailPlugin())
|
||||
|
||||
# Start capturing events.
|
||||
ac1.log = cap.put
|
||||
|
||||
ac1.add_account_plugin(FailPlugin())
|
||||
# cause any event eg contact added/changed
|
||||
ac1.create_contact("something@example.org")
|
||||
res = cap.get(timeout=10)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-01-15
|
||||
2024-11-26
|
||||
@@ -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.84.0
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -11,7 +11,7 @@ set -euo pipefail
|
||||
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
tox -c python -e py --devenv venv
|
||||
venv/bin/pip install --upgrade pip
|
||||
|
||||
@@ -12,7 +12,7 @@ export DCC_RS_DEV=`pwd`
|
||||
|
||||
cd python
|
||||
|
||||
cargo build -p deltachat_ffi
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
# remove and inhibit writing PYC files
|
||||
rm -rf tests/__pycache__
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
|
||||
# compile core lib
|
||||
|
||||
cargo build --release -p deltachat_ffi
|
||||
cargo build --release -p deltachat_ffi --features jsonrpc
|
||||
|
||||
# Statically link against libdeltachat.a.
|
||||
export DCC_RS_DEV="$PWD"
|
||||
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
52
src/blob.rs
52
src/blob.rs
@@ -279,9 +279,7 @@ impl<'a> BlobObject<'a> {
|
||||
let ext: String = name
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| {
|
||||
(!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control()
|
||||
})
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.take(33)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
@@ -765,6 +763,7 @@ mod tests {
|
||||
use fs::File;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
@@ -984,10 +983,6 @@ mod tests {
|
||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
|
||||
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
|
||||
assert_eq!(ext, ".pdf");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1463,21 +1458,36 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
let bytes = include_bytes!("../test-data/image/image100x50.gif");
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||
// extension.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
944
src/chat.rs
944
src/chat.rs
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,7 @@ impl Chatlist {
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9
|
||||
AND c.blocked!=1
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
@@ -261,7 +261,7 @@ impl Chatlist {
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
@@ -550,7 +550,7 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.len() == 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
@@ -576,7 +576,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -585,7 +585,7 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -597,7 +597,7 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.len() == 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -80,7 +80,7 @@ pub enum Config {
|
||||
/// SMTP server security (e.g. TLS, STARTTLS).
|
||||
SendSecurity,
|
||||
|
||||
/// Deprecated option for backwards compatibility.
|
||||
/// Deprecated option for backwards compatibilty.
|
||||
///
|
||||
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
|
||||
SmtpCertificateChecks,
|
||||
@@ -143,7 +143,7 @@ pub enum Config {
|
||||
/// Send BCC copy to self.
|
||||
///
|
||||
/// Should be enabled for multidevice setups.
|
||||
/// Default is 0 for chatmail accounts before a backup export, 1 otherwise.
|
||||
#[strum(props(default = "1"))]
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
@@ -202,7 +202,7 @@ pub enum Config {
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
///
|
||||
/// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise.
|
||||
/// Default is 1 for chatmail accounts before a backup export, 0 otherwise.
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
@@ -441,12 +441,6 @@ pub enum Config {
|
||||
/// Enable webxdc realtime features.
|
||||
#[strum(props(default = "1"))]
|
||||
WebxdcRealtimeEnabled,
|
||||
|
||||
/// Last device token stored on the chatmail server.
|
||||
///
|
||||
/// If it has not changed, we do not store
|
||||
/// the device token again.
|
||||
DeviceToken,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -519,19 +513,11 @@ impl Context {
|
||||
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
|
||||
false => Some("1"),
|
||||
true => Some("0"),
|
||||
},
|
||||
Config::ConfiguredInboxFolder => Some("INBOX"),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
&& Box::pin(self.is_chatmail()).await?
|
||||
{
|
||||
true => Some("1"),
|
||||
false => Some("0"),
|
||||
}
|
||||
}
|
||||
Config::DeleteServerAfter => match Box::pin(self.is_chatmail()).await? {
|
||||
false => Some("0"),
|
||||
true => Some("1"),
|
||||
},
|
||||
_ => key.get_str("default"),
|
||||
};
|
||||
Ok(val.map(|s| s.to_string()))
|
||||
@@ -1113,28 +1099,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_server_after_default() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
|
||||
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
|
||||
// does).
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
|
||||
@@ -61,7 +61,10 @@ macro_rules! progress {
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> Result<bool> {
|
||||
self.sql.get_raw_config_bool("configured").await
|
||||
self.sql
|
||||
.get_raw_config_bool("configured")
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
@@ -449,9 +452,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
let create = true;
|
||||
imap_session
|
||||
.select_with_uidvalidity(ctx, "INBOX", create)
|
||||
.select_with_uidvalidity(ctx, "INBOX")
|
||||
.await
|
||||
.context("could not read INBOX status")?;
|
||||
|
||||
|
||||
127
src/contact.rs
127
src/contact.rs
@@ -1,7 +1,7 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::{min, Reverse};
|
||||
use std::collections::{BinaryHeap, HashSet};
|
||||
use std::collections::BinaryHeap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
@@ -34,6 +34,7 @@ use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
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, smeared_time, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
@@ -113,8 +114,7 @@ impl ContactId {
|
||||
SET gossiped_timestamp=0
|
||||
WHERE EXISTS (SELECT 1 FROM chats_contacts
|
||||
WHERE chats_contacts.chat_id=chats.id
|
||||
AND chats_contacts.contact_id=?
|
||||
AND chats_contacts.add_timestamp >= chats_contacts.remove_timestamp)",
|
||||
AND chats_contacts.contact_id=?)",
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
@@ -129,19 +129,22 @@ impl ContactId {
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?;
|
||||
for id in ids {
|
||||
stmt.execute((origin, id))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
|
||||
sql::repeat_vars(ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[origin])
|
||||
.chain(params_iter(ids))
|
||||
.chain(params_iter(&[origin])),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns contact address.
|
||||
/// Returns contact adress.
|
||||
pub async fn addr(&self, context: &Context) -> Result<String> {
|
||||
let addr = context
|
||||
.sql
|
||||
@@ -1039,11 +1042,7 @@ impl Contact {
|
||||
listflags: u32,
|
||||
query: Option<&str>,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let self_addrs = context
|
||||
.get_all_self_addrs()
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
let self_addrs = context.get_all_self_addrs().await?;
|
||||
let mut add_self = false;
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
@@ -1058,32 +1057,29 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id, c.addr FROM contacts c
|
||||
&format!(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.id>?
|
||||
WHERE c.addr NOT IN ({})
|
||||
AND c.id>? \
|
||||
AND c.origin>=? \
|
||||
AND c.blocked=0 \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
(
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
minimal_origin,
|
||||
&s3str_like_cmd,
|
||||
&s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((id, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (id, addr) = row?;
|
||||
if !self_addrs.contains(&addr) {
|
||||
ret.push(id);
|
||||
}
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 }
|
||||
])),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -1116,23 +1112,23 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, addr FROM contacts
|
||||
WHERE id>?
|
||||
&format!(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr NOT IN ({})
|
||||
AND id>?
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
(ContactId::LAST_SPECIAL, minimal_origin),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((id, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (id, addr) = row?;
|
||||
if !self_addrs.contains(&addr) {
|
||||
ret.push(id);
|
||||
}
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&self_addrs)
|
||||
.chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
|
||||
),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -1988,7 +1984,7 @@ mod tests {
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_contact_id_values() {
|
||||
@@ -2919,8 +2915,6 @@ Hi."#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -2936,7 +2930,18 @@ Hi."#;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
assert!(contact.was_seen_recently());
|
||||
let green = nu_ansi_term::Color::Green.normal();
|
||||
assert!(
|
||||
contact.was_seen_recently(),
|
||||
"{}",
|
||||
green.paint(
|
||||
"\nNOTE: This test failure is probably a false-positive, caused by tests running in parallel.
|
||||
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
|
||||
Until the false-positive is fixed:
|
||||
- Use `cargo test -- --test-threads 1` instead of `cargo test`
|
||||
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n"
|
||||
)
|
||||
);
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
@@ -3239,20 +3244,4 @@ Hi."#;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_is_verified() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.is_profile_verified(&alice).await?);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
|
||||
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use pgp::SignedPublicKey;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
use tokio::sync::{Mutex, Notify, OnceCell, RwLock};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
|
||||
@@ -292,7 +292,7 @@ pub struct InnerContext {
|
||||
pub(crate) push_subscribed: AtomicBool,
|
||||
|
||||
/// Iroh for realtime peer channels.
|
||||
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
|
||||
pub(crate) iroh: OnceCell<Iroh>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -450,7 +450,7 @@ impl Context {
|
||||
debug_logging: std::sync::RwLock::new(None),
|
||||
push_subscriber,
|
||||
push_subscribed: AtomicBool::new(false),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
iroh: OnceCell::new(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -486,19 +486,6 @@ impl Context {
|
||||
/// Stops the IO scheduler.
|
||||
pub async fn stop_io(&self) {
|
||||
self.scheduler.stop(self).await;
|
||||
if let Some(iroh) = self.iroh.write().await.take() {
|
||||
// Close all QUIC connections.
|
||||
|
||||
// Spawn into a separate task,
|
||||
// because Iroh calls `wait_idle()` internally
|
||||
// and it may take time, especially if the network
|
||||
// has become unavailable.
|
||||
tokio::spawn(async move {
|
||||
// We do not log the error because we do not want the task
|
||||
// to hold the reference to Context.
|
||||
let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
@@ -509,7 +496,7 @@ impl Context {
|
||||
|
||||
/// Indicate that the network likely has come back.
|
||||
pub async fn maybe_network(&self) {
|
||||
if let Some(ref iroh) = *self.iroh.read().await {
|
||||
if let Some(iroh) = self.iroh.get() {
|
||||
iroh.network_change().await;
|
||||
}
|
||||
self.scheduler.maybe_network().await;
|
||||
@@ -553,7 +540,23 @@ impl Context {
|
||||
|
||||
if self.scheduler.is_running().await {
|
||||
self.scheduler.maybe_network().await;
|
||||
self.wait_for_all_work_done().await;
|
||||
|
||||
// Wait until fetching is finished.
|
||||
// Ideally we could wait for connectivity change events,
|
||||
// but sleep loop is good enough.
|
||||
|
||||
// First 100 ms sleep in chunks of 10 ms.
|
||||
for _ in 0..10 {
|
||||
if self.all_work_done().await {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
// If we are not finished in 100 ms, keep waking up every 100 ms.
|
||||
while !self.all_work_done().await {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
} else {
|
||||
// Pause the scheduler to ensure another connection does not start
|
||||
// while we are fetching on a dedicated connection.
|
||||
@@ -643,36 +646,14 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Emits a MsgsChanged event with specified chat and message ids
|
||||
///
|
||||
/// If IDs are unset, [`Self::emit_msgs_changed_without_ids`]
|
||||
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
|
||||
/// instead of this function.
|
||||
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
debug_assert!(!msg_id.is_unset());
|
||||
|
||||
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 a MsgsChanged event with specified chat and without message id.
|
||||
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
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) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
debug_assert!(!msg_id.is_unset());
|
||||
|
||||
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
chatlist_events::emit_chatlist_item_changed(self, chat_id);
|
||||
@@ -1777,7 +1758,6 @@ mod tests {
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -201,11 +201,7 @@ impl Session {
|
||||
bail!("Attempt to fetch UID 0");
|
||||
}
|
||||
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
128
src/e2ee.rs
128
src/e2ee.rs
@@ -40,17 +40,20 @@ impl EncryptHelper {
|
||||
|
||||
/// Determines if we can and should encrypt.
|
||||
///
|
||||
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
|
||||
/// of peerstates should prefer encryption. Own preference is counted equally to peer
|
||||
/// preferences, even if message copy is not sent to self.
|
||||
///
|
||||
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
|
||||
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
|
||||
///
|
||||
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
|
||||
pub(crate) async fn should_encrypt(
|
||||
pub fn should_encrypt(
|
||||
&self,
|
||||
context: &Context,
|
||||
e2ee_guaranteed: bool,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<bool> {
|
||||
let is_chatmail = context.is_chatmail().await?;
|
||||
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
1
|
||||
} else {
|
||||
@@ -61,15 +64,10 @@ impl EncryptHelper {
|
||||
Some(peerstate) => {
|
||||
let prefer_encrypt = peerstate.prefer_encrypt;
|
||||
info!(context, "Peerstate for {addr:?} is {prefer_encrypt}.");
|
||||
if match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {
|
||||
(peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail)
|
||||
&& self.prefer_encrypt == EncryptPreference::Mutual
|
||||
}
|
||||
EncryptPreference::Mutual => true,
|
||||
} {
|
||||
prefer_encrypt_count += 1;
|
||||
}
|
||||
match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
|
||||
EncryptPreference::Mutual => prefer_encrypt_count += 1,
|
||||
};
|
||||
}
|
||||
None => {
|
||||
let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
|
||||
@@ -172,11 +170,9 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::key::DcKey;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{bob_keypair, TestContext, TestContextManager};
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
@@ -324,109 +320,29 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt() -> Result<()> {
|
||||
async fn test_should_encrypt() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert!(t.get_config_bool(Config::E2eeEnabled).await?);
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
// test with EncryptPreference::NoPreference:
|
||||
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
// Own preference is `Mutual` and we have the peer's key.
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with EncryptPreference::Reset
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with EncryptPreference::Mutual (self is also Mutual)
|
||||
let ps = new_peerstates(EncryptPreference::Mutual);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
|
||||
// test with missing peerstate
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt_e2ee_disabled() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::E2eeEnabled, false).await?;
|
||||
let encrypt_helper = EncryptHelper::new(t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await?);
|
||||
|
||||
let mut ps = new_peerstates(EncryptPreference::Mutual);
|
||||
// Own preference is `NoPreference` and there's no majority with `Mutual`.
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
// Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we
|
||||
// can't send unencrypted, e.g. protected groups.
|
||||
ps.push(ps[0].clone());
|
||||
assert!(encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
// Test with missing peerstate.
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_prefers_to_encrypt() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
let bob_chat_id = tcm
|
||||
.send_recv_accept(alice, bob, "Hello from DC")
|
||||
.await
|
||||
.chat_id;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
|
||||
\n\
|
||||
Hello from another MUA\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
|
||||
assert!(msg.get_showpadlock());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
let bob_chat_id = receive_imf(
|
||||
bob,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
|
||||
\n\
|
||||
Hello\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
|
||||
assert!(!msg.get_showpadlock());
|
||||
Ok(())
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
130
src/ephemeral.rs
130
src/ephemeral.rs
@@ -84,6 +84,7 @@ use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time, SystemTime};
|
||||
|
||||
@@ -328,44 +329,23 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
let should_interrupt =
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let mut should_interrupt = false;
|
||||
let mut stmt =
|
||||
transaction.prepare(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id=?2")?;
|
||||
for msg_id in msg_ids {
|
||||
should_interrupt |= stmt.execute((now, msg_id))? > 0;
|
||||
}
|
||||
Ok(should_interrupt)
|
||||
}).await?;
|
||||
if should_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts ephemeral timer for all messages in the chat.
|
||||
///
|
||||
/// This should be called when chat is marked as noticed.
|
||||
pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
let now = time();
|
||||
let should_interrupt = context
|
||||
let count = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE chat_id = ?2
|
||||
AND ephemeral_timer > 0
|
||||
AND (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer)",
|
||||
(now, chat_id),
|
||||
&format!(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::sql::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
|
||||
.chain(params_iter(msg_ids)),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
if should_interrupt {
|
||||
.await?;
|
||||
if count > 0 {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
@@ -502,7 +482,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
}
|
||||
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
|
||||
}
|
||||
|
||||
for msg_id in webxdc_deleted {
|
||||
@@ -715,9 +695,7 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
@@ -952,6 +930,7 @@ mod tests {
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -978,12 +957,14 @@ mod tests {
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends second message to Bob, with no timer
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
@@ -1444,77 +1425,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that ephemeral timer is started when the chat is noticed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_noticed_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that archiving the chat starts ephemeral timer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
bob_received_message
|
||||
.chat_id
|
||||
.set_visibility(bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
|
||||
// Bob mutes the chat so it is not unarchived.
|
||||
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
||||
|
||||
// Now test that for already archived chat
|
||||
// timer is started if all archived chats are marked as noticed.
|
||||
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
||||
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
||||
|
||||
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(
|
||||
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub enum EventType {
|
||||
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a message box then.
|
||||
/// in a messasge box then.
|
||||
Error(String),
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
@@ -109,9 +109,6 @@ pub enum EventType {
|
||||
|
||||
/// A webxdc wants an info message or a changed summary to be notified.
|
||||
IncomingWebxdcNotify {
|
||||
/// ID of the chat.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the contact sending.
|
||||
contact_id: ContactId,
|
||||
|
||||
|
||||
@@ -65,15 +65,6 @@ pub enum HeaderDef {
|
||||
ChatGroupMemberAdded,
|
||||
ChatContent,
|
||||
|
||||
/// Past members of the group.
|
||||
ChatGroupPastMembers,
|
||||
|
||||
/// Space-separated timestamps of member addition
|
||||
/// for members listed in the `To` field
|
||||
/// followed by timestamps of member removal
|
||||
/// for members listed in the `Chat-Group-Past-Members` field.
|
||||
ChatGroupMemberTimestamps,
|
||||
|
||||
/// Duration of the attached media file.
|
||||
ChatDuration,
|
||||
|
||||
@@ -82,7 +73,6 @@ pub enum HeaderDef {
|
||||
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
AutocryptGossip,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
|
||||
|
||||
78
src/html.rs
78
src/html.rs
@@ -7,8 +7,6 @@
|
||||
//! `MsgId.get_html()` will return HTML -
|
||||
//! this allows nice quoting, handling linebreaks properly etc.
|
||||
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use lettre_email::mime::Mime;
|
||||
@@ -79,26 +77,21 @@ fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
|
||||
struct HtmlMsgParser {
|
||||
pub html: String,
|
||||
pub plain: Option<PlainText>,
|
||||
pub(crate) msg_html: String,
|
||||
}
|
||||
|
||||
impl HtmlMsgParser {
|
||||
/// Function takes a raw mime-message string,
|
||||
/// searches for the main-text part
|
||||
/// and returns that as parser.html
|
||||
pub async fn from_bytes<'a>(
|
||||
context: &Context,
|
||||
rawmime: &'a [u8],
|
||||
) -> Result<(Self, mailparse::ParsedMail<'a>)> {
|
||||
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
|
||||
let mut parser = HtmlMsgParser {
|
||||
html: "".to_string(),
|
||||
plain: None,
|
||||
msg_html: "".to_string(),
|
||||
};
|
||||
|
||||
let parsedmail = mailparse::parse_mail(rawmime).context("Failed to parse mail")?;
|
||||
let parsedmail = mailparse::parse_mail(rawmime)?;
|
||||
|
||||
parser.collect_texts_recursive(context, &parsedmail).await?;
|
||||
parser.collect_texts_recursive(&parsedmail).await?;
|
||||
|
||||
if parser.html.is_empty() {
|
||||
if let Some(plain) = &parser.plain {
|
||||
@@ -107,8 +100,8 @@ impl HtmlMsgParser {
|
||||
} else {
|
||||
parser.cid_to_data_recursive(context, &parsedmail).await?;
|
||||
}
|
||||
parser.html += &mem::take(&mut parser.msg_html);
|
||||
Ok((parser, parsedmail))
|
||||
|
||||
Ok(parser)
|
||||
}
|
||||
|
||||
/// Function iterates over all mime-parts
|
||||
@@ -121,13 +114,12 @@ impl HtmlMsgParser {
|
||||
/// therefore we use the first one.
|
||||
async fn collect_texts_recursive<'a>(
|
||||
&'a mut self,
|
||||
context: &'a Context,
|
||||
mail: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Result<()> {
|
||||
match get_mime_multipart_type(&mail.ctype) {
|
||||
MimeMultipartType::Multiple => {
|
||||
for cur_data in &mail.subparts {
|
||||
Box::pin(self.collect_texts_recursive(context, cur_data)).await?
|
||||
Box::pin(self.collect_texts_recursive(cur_data)).await?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -136,35 +128,8 @@ impl HtmlMsgParser {
|
||||
if raw.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let (parser, mail) = Box::pin(HtmlMsgParser::from_bytes(context, &raw)).await?;
|
||||
if !parser.html.is_empty() {
|
||||
let mut text = "\r\n\r\n".to_string();
|
||||
for h in mail.headers {
|
||||
let key = h.get_key();
|
||||
if matches!(
|
||||
key.to_lowercase().as_str(),
|
||||
"date"
|
||||
| "from"
|
||||
| "sender"
|
||||
| "reply-to"
|
||||
| "to"
|
||||
| "cc"
|
||||
| "bcc"
|
||||
| "subject"
|
||||
) {
|
||||
text += &format!("{key}: {}\r\n", h.get_value());
|
||||
}
|
||||
}
|
||||
text += "\r\n";
|
||||
self.msg_html += &PlainText {
|
||||
text,
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html();
|
||||
self.msg_html += &parser.html;
|
||||
}
|
||||
Ok(())
|
||||
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>()?;
|
||||
@@ -210,7 +175,14 @@ impl HtmlMsgParser {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
MimeMultipartType::Message => Ok(()),
|
||||
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")?;
|
||||
Box::pin(self.cid_to_data_recursive(context, &mail)).await
|
||||
}
|
||||
MimeMultipartType::Single => {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
if mimetype.type_() == mime::IMAGE {
|
||||
@@ -268,7 +240,7 @@ impl MsgId {
|
||||
warn!(context, "get_html: parser error: {:#}", err);
|
||||
Ok(None)
|
||||
}
|
||||
Ok((parser, _)) => Ok(Some(parser.html)),
|
||||
Ok(parser) => Ok(Some(parser.html)),
|
||||
}
|
||||
} else {
|
||||
warn!(context, "get_html: no mime for {}", self);
|
||||
@@ -302,7 +274,7 @@ mod tests {
|
||||
async fn test_htmlparse_plain_unspecified() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -320,7 +292,7 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
async fn test_htmlparse_plain_iso88591() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -338,7 +310,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
async fn test_htmlparse_plain_flowed() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(parser.plain.unwrap().flowed);
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
@@ -360,7 +332,7 @@ and will be wrapped as usual.<br/>
|
||||
async fn test_htmlparse_alt_plain() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -381,7 +353,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_htmlparse_html() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_html.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
|
||||
// on windows, `\r\n` linends are returned from mimeparser,
|
||||
// however, rust multiline-strings use just `\n`;
|
||||
@@ -399,7 +371,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_htmlparse_alt_html() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
|
||||
r##"<html>
|
||||
@@ -414,7 +386,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_htmlparse_alt_plain_html() {
|
||||
let t = TestContext::new().await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
|
||||
r##"<html>
|
||||
@@ -439,7 +411,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(test.find("data:").is_none());
|
||||
|
||||
// parsing converts cid: to data:
|
||||
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert!(parser.html.contains("<html>"));
|
||||
assert!(!parser.html.contains("Content-Id:"));
|
||||
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));
|
||||
|
||||
281
src/imap.rs
281
src/imap.rs
@@ -13,7 +13,7 @@ use std::{
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
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::ContactAddress;
|
||||
@@ -41,11 +41,11 @@ use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::push::encrypt_device_token;
|
||||
use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
};
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{self, create_id, duration_to_str};
|
||||
|
||||
@@ -407,7 +407,7 @@ impl Imap {
|
||||
"IMAP-LOGIN as {}",
|
||||
lp.user
|
||||
)));
|
||||
self.connectivity.set_preparing(context).await;
|
||||
self.connectivity.set_connected(context).await;
|
||||
info!(context, "Successfully logged into IMAP server");
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -540,14 +540,10 @@ impl Imap {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let create = false;
|
||||
let folder_exists = session
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
session
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
if !folder_exists {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !session.new_mail && !fetch_existing_msgs {
|
||||
info!(context, "No new emails in folder {folder:?}.");
|
||||
@@ -839,52 +835,45 @@ impl Session {
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<()> {
|
||||
let uid_validity;
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msgs = BTreeMap::new();
|
||||
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
if folder_exists {
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
.await
|
||||
.with_context(|| format!("Can't resync folder {folder}"))?;
|
||||
while let Some(fetch) = list.try_next().await? {
|
||||
let headers = match get_fetch_headers(&fetch) {
|
||||
Ok(headers) => headers,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse FETCH headers: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let message_id = prefetch_get_message_id(&headers);
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
|
||||
msgs.insert(
|
||||
uid,
|
||||
(
|
||||
rfc724_mid,
|
||||
target_folder(context, folder, folder_meaning, &headers).await?,
|
||||
),
|
||||
);
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
.await
|
||||
.with_context(|| format!("can't resync folder {folder}"))?;
|
||||
while let Some(fetch) = list.try_next().await? {
|
||||
let headers = match get_fetch_headers(&fetch) {
|
||||
Ok(headers) => headers,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse FETCH headers: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let message_id = prefetch_get_message_id(&headers);
|
||||
|
||||
if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
|
||||
msgs.insert(
|
||||
uid,
|
||||
(
|
||||
rfc724_mid,
|
||||
target_folder(context, folder, folder_meaning, &headers).await?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"resync_folder_uids: Collected {} message IDs in {folder}.",
|
||||
msgs.len(),
|
||||
);
|
||||
|
||||
uid_validity = get_uidvalidity(context, folder).await?;
|
||||
} else {
|
||||
warn!(context, "resync_folder_uids: No folder {folder}.");
|
||||
uid_validity = 0;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Resync: collected {} message IDs in folder {}",
|
||||
msgs.len(),
|
||||
folder,
|
||||
);
|
||||
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
|
||||
// Write collected UIDs to SQLite database.
|
||||
context
|
||||
.sql
|
||||
@@ -921,15 +910,15 @@ impl Session {
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.await
|
||||
.context("Cannot remove deleted messages from imap table")?;
|
||||
.context("cannot remove deleted messages from imap table")?;
|
||||
|
||||
context.emit_event(EventType::ImapMessageDeleted(format!(
|
||||
"IMAP messages {uid_set} marked as deleted"
|
||||
@@ -952,15 +941,15 @@ impl Session {
|
||||
// Messages are moved or don't exist, IMAP returns OK response in both cases.
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.await
|
||||
.context("Cannot delete moved messages from imap table")?;
|
||||
.context("cannot delete moved messages from imap table")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} moved to {target}"
|
||||
)));
|
||||
@@ -1006,15 +995,15 @@ impl Session {
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE imap SET target='' WHERE id IN ({})",
|
||||
sql::repeat_vars(row_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(row_ids),
|
||||
)
|
||||
.await
|
||||
.context("Cannot plan deletion of messages")?;
|
||||
.context("cannot plan deletion of messages")?;
|
||||
if copy {
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
@@ -1050,11 +1039,7 @@ impl Session {
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
@@ -1148,40 +1133,29 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
let create = false;
|
||||
let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
Ok(folder_exists) => folder_exists,
|
||||
};
|
||||
if !folder_exists {
|
||||
warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
|
||||
if let Err(err) = self.select_with_uidvalidity(context, &folder).await {
|
||||
warn!(context, "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
|
||||
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
|
||||
continue;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Marked messages {} in folder {} as seen.", uid_set, folder
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"DELETE FROM imap_markseen WHERE id IN ({})",
|
||||
sql::repeat_vars(rowid_set.len())
|
||||
),
|
||||
rusqlite::params_from_iter(rowid_set),
|
||||
)
|
||||
.await
|
||||
.context("cannot remove messages marked as seen from imap_markseen table")?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
|
||||
for rowid in rowid_set {
|
||||
stmt.execute((rowid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Cannot remove messages marked as seen from imap_markseen table")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1197,14 +1171,9 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
self.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("Failed to select folder")?;
|
||||
if !folder_exists {
|
||||
return Ok(());
|
||||
}
|
||||
.context("failed to select folder")?;
|
||||
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
@@ -1452,7 +1421,9 @@ impl Session {
|
||||
|
||||
let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
|
||||
let rfc724_mid = if let Some(rfc724_mid) = uid_message_ids.get(&request_uid) {
|
||||
rfc724_mid
|
||||
} else {
|
||||
error!(
|
||||
context,
|
||||
"No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
|
||||
@@ -1589,52 +1560,16 @@ impl Session {
|
||||
};
|
||||
|
||||
if self.can_metadata() && self.can_push() {
|
||||
let device_token_changed =
|
||||
context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
if device_token_changed {
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
let encrypted_device_token = encrypt_device_token(&device_token)
|
||||
.context("Failed to encrypt device token")?;
|
||||
|
||||
// We expect that the server supporting `XDELTAPUSH` capability
|
||||
// has non-synchronizing literals support as well:
|
||||
// <https://www.rfc-editor.org/rfc/rfc7888>.
|
||||
let encrypted_device_token_len = encrypted_device_token.len();
|
||||
|
||||
if encrypted_device_token_len <= 4096 {
|
||||
self.run_command_and_check_ok(&format_setmetadata(
|
||||
&folder,
|
||||
&encrypted_device_token,
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
|
||||
// Store device token saved on the server
|
||||
// to prevent storing duplicate tokens.
|
||||
// The server cannot deduplicate on its own
|
||||
// because encryption gives a different
|
||||
// result each time.
|
||||
context
|
||||
.set_config_internal(Config::DeviceToken, Some(&device_token))
|
||||
.await?;
|
||||
} else {
|
||||
// If Apple or Google (FCM) gives us a very large token,
|
||||
// do not even try to give it to IMAP servers.
|
||||
//
|
||||
// Limit of 4096 is arbitrarily selected
|
||||
// to be the same as required by LITERAL- IMAP extension.
|
||||
//
|
||||
// Dovecot supports LITERAL+ and non-synchronizing literals
|
||||
// of any length, but there is no reason for tokens
|
||||
// to be that large even after OpenPGP encryption.
|
||||
warn!(context, "Device token is too long for LITERAL-, ignoring.");
|
||||
}
|
||||
}
|
||||
self.run_command_and_check_ok(format!(
|
||||
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||
} else if !context.push_subscriber.heartbeat_subscribed().await {
|
||||
let context = context.clone();
|
||||
@@ -1646,17 +1581,10 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_setmetadata(folder: &str, device_token: &str) -> String {
|
||||
let device_token_len = device_token.len();
|
||||
format!(
|
||||
"SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
|
||||
)
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Returns success if we successfully set the flag or we otherwise
|
||||
/// think add_flag should not be retried: Disconnection during setting
|
||||
/// the flag, or other imap-errors, returns Ok as well.
|
||||
/// the flag, or other imap-errors, returns true as well.
|
||||
///
|
||||
/// Returning error means that the operation can be retried.
|
||||
async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
|
||||
@@ -1703,11 +1631,7 @@ impl Session {
|
||||
self.close().await?;
|
||||
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
|
||||
// emails moved before that wouldn't be fetched but considered "old" instead.
|
||||
let create = false;
|
||||
let folder_exists = self
|
||||
.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
}
|
||||
@@ -1718,10 +1642,7 @@ impl Session {
|
||||
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
|
||||
// the variants here.
|
||||
for folder in folders {
|
||||
match self
|
||||
.select_with_uidvalidity(context, folder, create_mvbox)
|
||||
.await
|
||||
{
|
||||
match self.select_with_uidvalidity(context, folder).await {
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
@@ -2572,14 +2493,10 @@ async fn add_all_recipients_as_contacts(
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
let create = false;
|
||||
let folder_exists = session
|
||||
.select_with_uidvalidity(context, &mailbox, create)
|
||||
session
|
||||
.select_with_uidvalidity(context, &mailbox)
|
||||
.await
|
||||
.with_context(|| format!("could not select {mailbox}"))?;
|
||||
if !folder_exists {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let recipients = session
|
||||
.get_all_recipients(context)
|
||||
@@ -2947,16 +2864,4 @@ mod tests {
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setmetadata_device_token() {
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foobarbaz"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
||||
);
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ impl Session {
|
||||
) -> Result<Self> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
let create = true;
|
||||
self.select_with_uidvalidity(context, folder, create)
|
||||
.await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
self.new_mail = true;
|
||||
|
||||
@@ -34,7 +34,6 @@ impl Imap {
|
||||
let watched_folders = get_watched_folders(context).await?;
|
||||
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
let mut folder_names = Vec::new();
|
||||
|
||||
for folder in folders {
|
||||
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
|
||||
@@ -45,7 +44,6 @@ impl Imap {
|
||||
// already been moved and left it in the inbox.
|
||||
continue;
|
||||
}
|
||||
folder_names.push(folder.name().to_string());
|
||||
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
|
||||
|
||||
if let Some(config) = folder_meaning.to_config() {
|
||||
@@ -93,7 +91,6 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Found folders: {folder_names:?}.");
|
||||
last_scan.replace(tools::Time::now());
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -111,8 +111,7 @@ impl ImapSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a folder optionally creating it and takes care of UIDVALIDITY changes. Returns false
|
||||
/// iff `folder` doesn't exist.
|
||||
/// Selects a folder and takes care of UIDVALIDITY changes.
|
||||
///
|
||||
/// When selecting a folder for the first time, sets the uid_next to the current
|
||||
/// mailbox.uid_next so that no old emails are fetched.
|
||||
@@ -124,24 +123,11 @@ impl ImapSession {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
create: bool,
|
||||
) -> Result<bool> {
|
||||
let newly_selected = if create {
|
||||
self.select_or_create_folder(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to select or create folder {folder}"))?
|
||||
} else {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => newly_selected,
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => return Ok(false),
|
||||
_ => {
|
||||
return Err(err)
|
||||
.with_context(|| format!("failed to select folder {folder}"))?
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
) -> Result<()> {
|
||||
let newly_selected = self
|
||||
.select_or_create_folder(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to select or create folder {folder}"))?;
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
.as_mut()
|
||||
@@ -213,7 +199,7 @@ impl ImapSession {
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// UIDVALIDITY is modified, reset highest seen MODSEQ.
|
||||
@@ -247,7 +233,7 @@ impl ImapSession {
|
||||
old_uid_next,
|
||||
old_uid_validity,
|
||||
);
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
src/imex.rs
35
src/imex.rs
@@ -416,7 +416,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
.context("cannot import unpacked database");
|
||||
}
|
||||
if res.is_ok() {
|
||||
res = adjust_bcc_self(context).await;
|
||||
res = adjust_delete_server_after(context).await;
|
||||
}
|
||||
fs::remove_file(unpacked_database)
|
||||
.await
|
||||
@@ -796,7 +796,7 @@ async fn export_database(
|
||||
.to_str()
|
||||
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
|
||||
|
||||
adjust_bcc_self(context).await?;
|
||||
adjust_delete_server_after(context).await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", timestamp)
|
||||
@@ -826,14 +826,15 @@ async fn export_database(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sets `Config::BccSelf` (and `DeleteServerAfter` to "never" in effect) if needed so that new
|
||||
/// messages are present on the server after a backup restoration or available for all devices in
|
||||
/// multi-device case. NB: Calling this after a backup import isn't reliable as we can crash in
|
||||
/// between, but this is a problem only for old backups, new backups already have `BccSelf` set if
|
||||
/// necessary.
|
||||
async fn adjust_bcc_self(context: &Context) -> Result<()> {
|
||||
if context.is_chatmail().await? && !context.config_exists(Config::BccSelf).await? {
|
||||
context.set_config(Config::BccSelf, Some("1")).await?;
|
||||
/// Sets `Config::DeleteServerAfter` to "never" if needed so that new messages are present on the
|
||||
/// server after a backup restoration or available for all devices in multi-device case.
|
||||
/// NB: Calling this after a backup import isn't reliable as we can crash in between, but this is a
|
||||
/// problem only for old backups, new backups already have `DeleteServerAfter` set if necessary.
|
||||
async fn adjust_delete_server_after(context: &Context) -> Result<()> {
|
||||
if context.is_chatmail().await? && !context.config_exists(Config::DeleteServerAfter).await? {
|
||||
context
|
||||
.set_config(Config::DeleteServerAfter, Some("0"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1029,20 +1030,12 @@ mod tests {
|
||||
|
||||
let context1 = &TestContext::new_alice().await;
|
||||
|
||||
// Check that the settings are displayed correctly.
|
||||
assert_eq!(
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
// Check that the setting is displayed correctly.
|
||||
assert_eq!(
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
assert_eq!(
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
context1.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("1".to_string())
|
||||
@@ -1065,10 +1058,6 @@ mod tests {
|
||||
assert!(context2.is_configured().await?);
|
||||
assert!(context2.is_chatmail().await?);
|
||||
for ctx in [context1, context2] {
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::BccSelf).await?,
|
||||
Some("1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.get_config(Config::DeleteServerAfter).await?,
|
||||
Some("0".to_string())
|
||||
|
||||
@@ -178,7 +178,6 @@ impl BackupProvider {
|
||||
}
|
||||
|
||||
info!(context, "Received valid backup authentication token.");
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
let blobdir = BlobDirContents::new(&context).await?;
|
||||
@@ -310,10 +309,6 @@ pub async fn get_backup2(
|
||||
let mut file_size_buf = [0u8; 8];
|
||||
recv_stream.read_exact(&mut file_size_buf).await?;
|
||||
let file_size = u64::from_be_bytes(file_size_buf);
|
||||
info!(context, "Received backup file size.");
|
||||
// Emit a nonzero progress so that UIs can display smth like "Transferring...".
|
||||
context.emit_event(EventType::ImexProgress(1));
|
||||
|
||||
import_backup_stream(context, recv_stream, file_size, passphrase)
|
||||
.await
|
||||
.context("Failed to import backup from QUIC stream")?;
|
||||
@@ -439,14 +434,12 @@ mod tests {
|
||||
assert!(msg.save_file(&ctx1, &path).await.is_err());
|
||||
|
||||
// Check that both received the ImexProgress events.
|
||||
for ctx in [&ctx0, &ctx1] {
|
||||
ctx.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1)))
|
||||
.await;
|
||||
ctx.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
ctx0.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
ctx1.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
64
src/key.rs
64
src/key.rs
@@ -30,39 +30,7 @@ use crate::tools::{self, time_elapsed};
|
||||
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
/// Create a key from some bytes.
|
||||
fn from_slice(bytes: &[u8]) -> Result<Self> {
|
||||
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
|
||||
if let Ok(res) = res {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// Workaround for keys imported using
|
||||
// Delta Chat core < 1.0.0.
|
||||
// Old Delta Chat core had a bug
|
||||
// that resulted in treating CRC24 checksum
|
||||
// as part of the key when reading ASCII Armor.
|
||||
// Some users that started using Delta Chat in 2019
|
||||
// have such corrupted keys with garbage bytes at the end.
|
||||
//
|
||||
// Garbage is at least 3 bytes long
|
||||
// and may be longer due to padding
|
||||
// at the end of the real key data
|
||||
// and importing the key multiple times.
|
||||
//
|
||||
// If removing 10 bytes is not enough,
|
||||
// the key is likely actually corrupted.
|
||||
for garbage_bytes in 3..std::cmp::min(bytes.len(), 10) {
|
||||
let res = <Self as Deserializable>::from_bytes(Cursor::new(
|
||||
bytes
|
||||
.get(..bytes.len().saturating_sub(garbage_bytes))
|
||||
.unwrap_or_default(),
|
||||
));
|
||||
if let Ok(res) = res {
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
|
||||
// Removing garbage bytes did not help, return the error.
|
||||
Ok(res?)
|
||||
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
|
||||
}
|
||||
|
||||
/// Create a key from a base64 string.
|
||||
@@ -597,36 +565,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests workaround for Delta Chat core < 1.0.0
|
||||
/// which parsed CRC24 at the end of ASCII Armor
|
||||
/// as the part of the key.
|
||||
/// Depending on the alignment and the number of
|
||||
/// `=` characters at the end of the key,
|
||||
/// this resulted in various number of garbage
|
||||
/// octets at the end of the key, starting from 3 octets,
|
||||
/// but possibly 4 or 5 and maybe more octets
|
||||
/// if the key is imported or transferred
|
||||
/// using Autocrypt Setup Message multiple times.
|
||||
#[test]
|
||||
fn test_ignore_trailing_garbage() {
|
||||
// Test several variants of garbage.
|
||||
for garbage in [
|
||||
b"\x02\xfc\xaa\x38\x4b\x5c".as_slice(),
|
||||
b"\x02\xfc\xaa".as_slice(),
|
||||
b"\x01\x02\x03\x04\x05".as_slice(),
|
||||
] {
|
||||
let private_key = KEYPAIR.secret.clone();
|
||||
|
||||
let mut binary = DcKey::to_bytes(&private_key);
|
||||
binary.extend(garbage);
|
||||
|
||||
let private_key2 =
|
||||
SignedSecretKey::from_slice(&binary).expect("Failed to ignore garbage");
|
||||
|
||||
assert_eq!(private_key.dc_fingerprint(), private_key2.dc_fingerprint());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base64_roundtrip() {
|
||||
let key = KEYPAIR.public.clone();
|
||||
|
||||
@@ -707,6 +707,9 @@ pub(crate) async fn save(
|
||||
))?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
|
||||
}
|
||||
@@ -1127,10 +1130,6 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
|
||||
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
|
||||
|
||||
// Location-only messages are "auto-generated", but they mustn't make the contact a bot.
|
||||
let contact = bob.add_or_lookup_contact(alice).await;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
// Day later Bob removes location.
|
||||
SystemTime::shift(Duration::from_secs(86400));
|
||||
delete_expired(alice, time()).await?;
|
||||
|
||||
176
src/message.rs
176
src/message.rs
@@ -293,7 +293,13 @@ impl MsgId {
|
||||
ret += ", Location sent";
|
||||
}
|
||||
|
||||
if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
|
||||
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
|
||||
|
||||
if 0 != e2ee_errors {
|
||||
if 0 != e2ee_errors & 0x2 {
|
||||
ret += ", Encrypted, no valid signature";
|
||||
}
|
||||
} else if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
|
||||
ret += ", Encrypted";
|
||||
}
|
||||
|
||||
@@ -342,7 +348,7 @@ impl MsgId {
|
||||
let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
|
||||
for server_url in server_urls {
|
||||
// Format as RFC 5092 relative IMAP URL.
|
||||
ret += &format!("\nServer-URL: {server_url}");
|
||||
ret += &format!("\n{server_url}");
|
||||
}
|
||||
}
|
||||
let hop_info = self.hop_info(context).await?;
|
||||
@@ -718,7 +724,7 @@ impl Message {
|
||||
/// `contact_id` set to [`ContactId::SELF`].
|
||||
///
|
||||
/// `latitude` is the North-south position of the location.
|
||||
/// `longitude` is the East-west position of the location.
|
||||
/// `longitutde` is the East-west position of the location.
|
||||
///
|
||||
/// [`location::set()`]: crate::location::set
|
||||
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
|
||||
@@ -947,6 +953,18 @@ impl Message {
|
||||
cmd != SystemMessage::Unknown
|
||||
}
|
||||
|
||||
/// Whether the message is still being created.
|
||||
///
|
||||
/// Messages with attachments might be created before the
|
||||
/// attachment is ready. In this case some more restrictions on
|
||||
/// the attachment apply, e.g. if the file to be attached is still
|
||||
/// being written to or otherwise will still change it can not be
|
||||
/// copied to the blobdir. Thus those attachments need to be
|
||||
/// created immediately in the blobdir with a valid filename.
|
||||
pub fn is_increation(&self) -> bool {
|
||||
self.viewtype.has_file() && self.state == MessageState::OutPreparing
|
||||
}
|
||||
|
||||
/// Returns true if the message is an Autocrypt Setup Message.
|
||||
pub fn is_setupmessage(&self) -> bool {
|
||||
if self.viewtype != Viewtype::File {
|
||||
@@ -1111,9 +1129,7 @@ impl Message {
|
||||
|
||||
/// Updates message state from the vCard attachment.
|
||||
pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
|
||||
let vcard = fs::read(path)
|
||||
.await
|
||||
.with_context(|| format!("Could not read {path:?}"))?;
|
||||
let vcard = fs::read(path).await.context("Could not read {path}")?;
|
||||
if let Some(summary) = get_vcard_summary(&vcard) {
|
||||
self.param.set(Param::Summary1, summary);
|
||||
} else {
|
||||
@@ -1609,15 +1625,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
trans.execute(
|
||||
let update_db = |conn: &mut rusqlite::Connection| {
|
||||
conn.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, msg.rfc724_mid),
|
||||
)?;
|
||||
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = context.sql.transaction(update_db).await {
|
||||
if let Err(e) = context.sql.call_write(update_db).await {
|
||||
error!(context, "delete_msgs: failed to update db: {e:#}.");
|
||||
res = Err(e);
|
||||
continue;
|
||||
@@ -1639,7 +1655,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
res?;
|
||||
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
|
||||
@@ -1669,12 +1685,12 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
|
||||
.await?;
|
||||
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for &id in &msg_ids {
|
||||
if let Some(msg) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.download_state as download_state,
|
||||
@@ -1685,39 +1701,39 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id=? AND m.chat_id>9",
|
||||
(id,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
(
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
ephemeral_timer,
|
||||
))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
msgs.push(msg);
|
||||
}
|
||||
}
|
||||
WHERE m.id IN ({}) AND m.chat_id>9",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&msg_ids),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
(
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
ephemeral_timer,
|
||||
))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if msgs
|
||||
.iter()
|
||||
@@ -2090,9 +2106,6 @@ pub enum Viewtype {
|
||||
Gif = 21,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
Sticker = 23,
|
||||
@@ -2193,6 +2206,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prepare_message_and_send() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
|
||||
let _msg2 = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert_eq!(_msg2.get_filemime(), None);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
@@ -2312,9 +2357,9 @@ mod tests {
|
||||
|
||||
let mut msg = Message::new_text("Quoted message".to_string());
|
||||
|
||||
// Send message, so it gets a Message-Id.
|
||||
// Prepare message for sending, so it gets a Message-Id.
|
||||
assert!(msg.rfc724_mid.is_empty());
|
||||
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
let msg_id = chat::prepare_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert!(!msg.rfc724_mid.is_empty());
|
||||
|
||||
@@ -2709,29 +2754,6 @@ mod tests {
|
||||
async fn test_is_bot() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice receives an auto-generated non-chat message.
|
||||
//
|
||||
// This could be a holiday notice,
|
||||
// in which case the message should be marked as bot-generated,
|
||||
// but the contact should not.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Claire <claire@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <789@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
assert!(msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
// Alice receives a message from Bob the bot.
|
||||
receive_imf(
|
||||
&alice,
|
||||
|
||||
@@ -66,36 +66,8 @@ pub struct MimeFactory {
|
||||
|
||||
selfstatus: String,
|
||||
|
||||
/// Vector of actual recipient addresses.
|
||||
///
|
||||
/// This is the list of addresses the message should be sent to.
|
||||
/// It is not the same as the `To` header,
|
||||
/// because in case of "member removed" message
|
||||
/// removed member is in the recipient list,
|
||||
/// but not in the `To` header.
|
||||
/// In case of broadcast lists there are multiple recipients,
|
||||
/// but the `To` header has no members.
|
||||
///
|
||||
/// If `bcc_self` configuration is enabled,
|
||||
/// this list will be extended with own address later,
|
||||
/// but `MimeFactory` is not responsible for this.
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Vector of pairs of recipient name and address that goes into the `To` field.
|
||||
///
|
||||
/// The list of actual message recipient addresses may be different,
|
||||
/// e.g. if members are hidden for broadcast lists.
|
||||
to: Vec<(String, String)>,
|
||||
|
||||
/// Vector of pairs of past group member names and addresses.
|
||||
past_members: Vec<(String, String)>,
|
||||
|
||||
/// Timestamps of the members in the same order as in the `recipients`
|
||||
/// followed by `past_members`.
|
||||
///
|
||||
/// If this is not empty, its length
|
||||
/// should be the sum of `recipients` and `past_members` length.
|
||||
member_timestamps: Vec<i64>,
|
||||
/// Vector of pairs of recipient name and address
|
||||
recipients: Vec<(String, String)>,
|
||||
|
||||
timestamp: i64,
|
||||
loaded: Loaded,
|
||||
@@ -156,7 +128,6 @@ impl MimeFactory {
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
||||
let undisclosed_recipients = chat.typ == Chattype::Broadcast;
|
||||
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let config_displayname = context
|
||||
@@ -174,101 +145,47 @@ impl MimeFactory {
|
||||
(name, None)
|
||||
};
|
||||
|
||||
let mut recipients = Vec::new();
|
||||
let mut to = Vec::new();
|
||||
let mut past_members = Vec::new();
|
||||
let mut member_timestamps = Vec::new();
|
||||
let mut recipients = Vec::with_capacity(5);
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let mut req_mdn = false;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
recipients.push(from_addr.to_string());
|
||||
to.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
}
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
.get(Param::ListPost)
|
||||
.context("Can't write to mailinglist without ListPost param")?;
|
||||
to.push(("".to_string(), list_post.to_string()));
|
||||
recipients.push(list_post.to_string());
|
||||
recipients.push(("".to_string(), list_post.to_string()));
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9 OR (cc.contact_id=1 AND ?)",
|
||||
(msg.chat_id, chat.typ == Chattype::Group),
|
||||
"SELECT c.authname, c.addr, c.id \
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
(msg.chat_id,),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
let id: ContactId = row.get(2)?;
|
||||
let add_timestamp: i64 = row.get(3)?;
|
||||
let remove_timestamp: i64 = row.get(4)?;
|
||||
Ok((authname, addr, id, add_timestamp, remove_timestamp))
|
||||
Ok((authname, addr, id))
|
||||
},
|
||||
|rows| {
|
||||
let mut past_member_timestamps = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let (authname, addr, id, add_timestamp, remove_timestamp) = row?;
|
||||
let addr = if id == ContactId::SELF {
|
||||
from_addr.to_string()
|
||||
} else {
|
||||
addr
|
||||
};
|
||||
let name = match attach_profile_data {
|
||||
true => authname,
|
||||
false => "".to_string(),
|
||||
};
|
||||
if add_timestamp >= remove_timestamp {
|
||||
if !recipients_contain_addr(&to, &addr) {
|
||||
recipients.push(addr.clone());
|
||||
if !undisclosed_recipients {
|
||||
to.push((name, addr));
|
||||
member_timestamps.push(add_timestamp);
|
||||
}
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
} else {
|
||||
// Row is a tombstone,
|
||||
// member is not actually part of the group.
|
||||
if !recipients_contain_addr(&past_members, &addr) {
|
||||
if let Some(email_to_remove) = email_to_remove {
|
||||
if email_to_remove == addr {
|
||||
// This is a "member removed" message,
|
||||
// we need to notify removed member
|
||||
// that it was removed.
|
||||
recipients.push(addr.clone());
|
||||
}
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
past_members.push((name, addr));
|
||||
past_member_timestamps.push(remove_timestamp);
|
||||
}
|
||||
}
|
||||
let (authname, addr, id) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
let name = match attach_profile_data {
|
||||
true => authname,
|
||||
false => "".to_string(),
|
||||
};
|
||||
recipients.push((name, addr));
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
}
|
||||
|
||||
debug_assert!(member_timestamps.len() >= to.len());
|
||||
|
||||
if to.len() > 1 {
|
||||
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
to.remove(position);
|
||||
member_timestamps.remove(position);
|
||||
}
|
||||
}
|
||||
|
||||
member_timestamps.extend(past_member_timestamps);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
@@ -309,19 +226,12 @@ impl MimeFactory {
|
||||
};
|
||||
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
|
||||
|
||||
debug_assert!(
|
||||
member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == member_timestamps.len()
|
||||
);
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
sender_displayname,
|
||||
selfstatus,
|
||||
recipients,
|
||||
to,
|
||||
past_members,
|
||||
member_timestamps,
|
||||
timestamp: msg.timestamp_sort,
|
||||
loaded: Loaded::Message { msg, chat },
|
||||
in_reply_to,
|
||||
@@ -349,10 +259,7 @@ impl MimeFactory {
|
||||
from_displayname: "".to_string(),
|
||||
sender_displayname: None,
|
||||
selfstatus: "".to_string(),
|
||||
recipients: vec![contact.get_addr().to_string()],
|
||||
to: vec![("".to_string(), contact.get_addr().to_string())],
|
||||
past_members: vec![],
|
||||
member_timestamps: vec![],
|
||||
recipients: vec![("".to_string(), contact.get_addr().to_string())],
|
||||
timestamp,
|
||||
loaded: Loaded::Mdn {
|
||||
rfc724_mid,
|
||||
@@ -376,7 +283,11 @@ impl MimeFactory {
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for addr in self.recipients.iter().filter(|&addr| *addr != self_addr) {
|
||||
for (_, addr) in self
|
||||
.recipients
|
||||
.iter()
|
||||
.filter(|(_, addr)| addr != &self_addr)
|
||||
{
|
||||
res.push((Peerstate::from_addr(context, addr).await?, addr.clone()));
|
||||
}
|
||||
|
||||
@@ -445,7 +356,7 @@ impl MimeFactory {
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
// Otherwise "smeared timestamps" may result in the condition
|
||||
// Othewise "smeared timestamps" may result in the condition
|
||||
// to fail even if the clock is monotonic.
|
||||
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
|
||||
Ok(true)
|
||||
@@ -564,7 +475,10 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
pub fn recipients(&self) -> Vec<String> {
|
||||
self.recipients.clone()
|
||||
self.recipients
|
||||
.iter()
|
||||
.map(|(_, addr)| addr.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
|
||||
@@ -574,33 +488,46 @@ impl MimeFactory {
|
||||
|
||||
let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
|
||||
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
let mut to = Vec::new();
|
||||
for (name, addr) in &self.to {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(new_address_with_name(name, addr.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut past_members = Vec::new(); // Contents of `Chat-Group-Past-Members` header.
|
||||
for (name, addr) in &self.past_members {
|
||||
if name.is_empty() {
|
||||
past_members.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
past_members.push(new_address_with_name(name, addr.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
self.member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == self.member_timestamps.len()
|
||||
);
|
||||
if to.is_empty() {
|
||||
if undisclosed_recipients {
|
||||
to.push(Address::new_group(
|
||||
"hidden-recipients".to_string(),
|
||||
Vec::new(),
|
||||
));
|
||||
} else {
|
||||
let email_to_remove = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => {
|
||||
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => None,
|
||||
};
|
||||
|
||||
for (name, addr) in &self.recipients {
|
||||
if let Some(email_to_remove) = email_to_remove {
|
||||
if email_to_remove == addr {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(new_address_with_name(name, addr.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Start with Internet Message Format headers in the order of the standard example
|
||||
@@ -613,26 +540,6 @@ impl MimeFactory {
|
||||
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
}
|
||||
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
|
||||
if !past_members.is_empty() {
|
||||
headers.push(
|
||||
Header::new_with_value("Chat-Group-Past-Members".into(), past_members.clone())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.member_timestamps.is_empty() {
|
||||
headers.push(
|
||||
Header::new_with_value(
|
||||
"Chat-Group-Member-Timestamps".into(),
|
||||
self.member_timestamps
|
||||
.iter()
|
||||
.map(|ts| ts.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
let encoded_subject = if subject_str
|
||||
@@ -745,9 +652,7 @@ impl MimeFactory {
|
||||
|
||||
let peerstates = self.peerstates_for_recipients(context).await?;
|
||||
let is_encrypted = !self.should_force_plaintext()
|
||||
&& encrypt_helper
|
||||
.should_encrypt(context, e2ee_guaranteed, &peerstates)
|
||||
.await?;
|
||||
&& encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
} else {
|
||||
@@ -1464,7 +1369,7 @@ impl MimeFactory {
|
||||
|
||||
// add attachment part
|
||||
if msg.viewtype.has_file() {
|
||||
let file_part = build_body_file(context, &msg).await?;
|
||||
let (file_part, _) = build_body_file(context, &msg, "").await?;
|
||||
parts.push(file_part);
|
||||
}
|
||||
|
||||
@@ -1604,10 +1509,14 @@ pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
.join("\r\n")
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder> {
|
||||
async fn build_body_file(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
base_name: &str,
|
||||
) -> Result<(PartBuilder, String)> {
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context)
|
||||
.get_blob(Param::File, context, true)
|
||||
.await?
|
||||
.context("msg has no file")?;
|
||||
let suffix = blob.suffix().unwrap_or("dat");
|
||||
@@ -1630,13 +1539,17 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
|
||||
),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"image_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
),
|
||||
if base_name.is_empty() {
|
||||
chrono::Utc
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.single()
|
||||
.map_or_else(
|
||||
|| "YY-mm-dd_hh:mm:ss".to_string(),
|
||||
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
|
||||
)
|
||||
} else {
|
||||
base_name.to_string()
|
||||
},
|
||||
&suffix,
|
||||
),
|
||||
Viewtype::Video => format!(
|
||||
@@ -1688,7 +1601,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.body(encoded_body);
|
||||
|
||||
Ok(mail)
|
||||
Ok((mail, filename_to_send))
|
||||
}
|
||||
|
||||
async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
|
||||
@@ -1992,7 +1905,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut new_msg = incoming_msg_to_reply_msg(
|
||||
let new_msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -2018,9 +1931,6 @@ mod tests {
|
||||
Original-Message-ID: <2893@example.com>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n", &t).await;
|
||||
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
||||
.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());
|
||||
@@ -2167,7 +2077,7 @@ mod tests {
|
||||
|
||||
let mut new_msg = Message::new_text("Hi".to_string());
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
||||
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
||||
|
||||
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
|
||||
|
||||
@@ -2224,7 +2134,7 @@ mod tests {
|
||||
) -> String {
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await;
|
||||
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 1).await;
|
||||
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
|
||||
|
||||
if delete_original_msg {
|
||||
incoming_msg.id.trash(&t, false).await.unwrap();
|
||||
@@ -2254,9 +2164,6 @@ mod tests {
|
||||
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
|
||||
}
|
||||
|
||||
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
||||
.await
|
||||
.unwrap();
|
||||
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
|
||||
mf.subject_str(&t).await.unwrap()
|
||||
}
|
||||
@@ -2277,6 +2184,9 @@ mod tests {
|
||||
|
||||
let mut new_msg = Message::new_text("Hi".to_string());
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::prepare_msg(context, chat_id, &mut new_msg)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
new_msg
|
||||
}
|
||||
@@ -2287,7 +2197,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let context = &t;
|
||||
|
||||
let mut msg = incoming_msg_to_reply_msg(
|
||||
let msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -2300,7 +2210,6 @@ mod tests {
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap();
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap();
|
||||
|
||||
@@ -2554,9 +2463,8 @@ mod tests {
|
||||
// Alice creates a group with Bob and Claire and then removes Bob.
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let claire_addr = "claire@foo.de";
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", "claire@foo.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
@@ -2572,17 +2480,10 @@ mod tests {
|
||||
.get_first_header("To")
|
||||
.context("no To: header parsed")?;
|
||||
let to = addrparse_header(to)?;
|
||||
for to_addr in to.iter() {
|
||||
match to_addr {
|
||||
mailparse::MailAddr::Single(ref info) => {
|
||||
// Addresses should be of existing members (Alice and Bob) and not Claire.
|
||||
assert_ne!(info.addr, claire_addr);
|
||||
}
|
||||
mailparse::MailAddr::Group(_) => {
|
||||
panic!("Group addresses are not expected here");
|
||||
}
|
||||
}
|
||||
}
|
||||
let mailbox = to
|
||||
.extract_single_info()
|
||||
.context("to: field does not contain exactly one address")?;
|
||||
assert_eq!(mailbox.addr, "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ use rand::distributions::{Alphanumeric, DistString};
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{add_info_msg, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{
|
||||
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
|
||||
@@ -35,9 +35,9 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::time;
|
||||
use crate::tools::{
|
||||
get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text, validate_id,
|
||||
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text,
|
||||
validate_id,
|
||||
};
|
||||
use crate::{chatlist_events, location, stock_str, tools};
|
||||
|
||||
@@ -58,14 +58,9 @@ pub(crate) struct MimeMessage {
|
||||
/// Message headers.
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
/// List of addresses from the `To` and `Cc` headers.
|
||||
///
|
||||
/// Addresses are normalized and lowercase.
|
||||
/// Addresses are normalized and lowercase
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
|
||||
/// List of addresses from the `Chat-Group-Past-Members` header.
|
||||
pub past_members: Vec<SingleInfo>,
|
||||
|
||||
/// `From:` address.
|
||||
pub from: SingleInfo,
|
||||
|
||||
@@ -111,26 +106,20 @@ pub(crate) struct MimeMessage {
|
||||
/// received.
|
||||
pub(crate) footer: Option<String>,
|
||||
|
||||
/// If set, this is a modified MIME message; clients should offer a way to view the original
|
||||
/// MIME message in this case.
|
||||
// if this flag is set, the parts/text/etc. are just close to the original mime-message;
|
||||
// clients should offer a way to view the original message in this case
|
||||
pub is_mime_modified: bool,
|
||||
|
||||
/// Decrypted, raw MIME structure. Nonempty iff `is_mime_modified` and the message was actually
|
||||
/// encrypted.
|
||||
/// The decrypted, raw mime structure.
|
||||
///
|
||||
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
|
||||
/// for e.g. late-parsing HTML.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
/// Hop info for debugging.
|
||||
pub(crate) hop_info: String,
|
||||
|
||||
/// Whether the message is auto-generated.
|
||||
///
|
||||
/// If chat message (with `Chat-Version` header) is auto-generated,
|
||||
/// the contact sending this should be marked as bot.
|
||||
///
|
||||
/// If non-chat message is auto-generated,
|
||||
/// it could be a holiday notice auto-reply,
|
||||
/// in which case the message should be marked as bot-generated,
|
||||
/// but the contact should not be.
|
||||
/// Whether the contact sending this should be marked as bot or non-bot.
|
||||
pub(crate) is_bot: Option<bool>,
|
||||
|
||||
/// When the message was received, in secs since epoch.
|
||||
@@ -238,7 +227,6 @@ impl MimeMessage {
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut recipients = Default::default();
|
||||
let mut past_members = Default::default();
|
||||
let mut from = Default::default();
|
||||
let mut list_post = Default::default();
|
||||
let mut chat_disposition_notification_to = None;
|
||||
@@ -248,7 +236,6 @@ impl MimeMessage {
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
@@ -269,7 +256,6 @@ impl MimeMessage {
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
@@ -447,8 +433,6 @@ impl MimeMessage {
|
||||
HeaderDef::ChatGroupAvatar,
|
||||
HeaderDef::ChatGroupMemberRemoved,
|
||||
HeaderDef::ChatGroupMemberAdded,
|
||||
HeaderDef::ChatGroupMemberTimestamps,
|
||||
HeaderDef::ChatGroupPastMembers,
|
||||
] {
|
||||
headers.remove(h.get_headername());
|
||||
}
|
||||
@@ -465,7 +449,6 @@ impl MimeMessage {
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut inner_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
@@ -523,7 +506,6 @@ impl MimeMessage {
|
||||
parts: Vec::new(),
|
||||
headers,
|
||||
recipients,
|
||||
past_members,
|
||||
list_post,
|
||||
from,
|
||||
from_is_signed,
|
||||
@@ -583,14 +565,11 @@ impl MimeMessage {
|
||||
},
|
||||
};
|
||||
|
||||
let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
|
||||
if parser.mdn_reports.is_empty()
|
||||
&& !is_location_only
|
||||
&& parser.sync_items.is_none()
|
||||
&& parser.webxdc_status_update.is_none()
|
||||
{
|
||||
let is_bot =
|
||||
parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
|
||||
if parser.mdn_reports.is_empty() && parser.webxdc_status_update.is_none() {
|
||||
// "Auto-Submitted" is also set by holiday-notices so we also check "chat-version".
|
||||
let is_bot = parser.headers.get("auto-submitted")
|
||||
== Some(&"auto-generated".to_string())
|
||||
&& parser.headers.contains_key("chat-version");
|
||||
parser.is_bot = Some(is_bot);
|
||||
}
|
||||
parser.maybe_remove_bad_parts();
|
||||
@@ -692,7 +671,7 @@ impl MimeMessage {
|
||||
&& self
|
||||
.parts
|
||||
.get(1)
|
||||
.is_some_and(|filepart| match filepart.typ {
|
||||
.map_or(false, |filepart| match filepart.typ {
|
||||
Viewtype::Image
|
||||
| Viewtype::Gif
|
||||
| Viewtype::Sticker
|
||||
@@ -1543,12 +1522,10 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn merge_headers(
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
past_members: &mut Vec<SingleInfo>,
|
||||
from: &mut Option<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
@@ -1577,11 +1554,6 @@ impl MimeMessage {
|
||||
if !recipients_new.is_empty() {
|
||||
*recipients = recipients_new;
|
||||
}
|
||||
let past_members_addresses =
|
||||
get_all_addresses_from_header(fields, "chat-group-past-members");
|
||||
if !past_members_addresses.is_empty() {
|
||||
*past_members = past_members_addresses;
|
||||
}
|
||||
let from_new = get_from(fields);
|
||||
if from_new.is_some() {
|
||||
*from = from_new;
|
||||
@@ -1693,8 +1665,18 @@ impl MimeMessage {
|
||||
.get_header_value(HeaderDef::MessageId)
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let mut to_list =
|
||||
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
|
||||
let to = if to_list.len() != 1 {
|
||||
// We do not know which recipient failed
|
||||
None
|
||||
} else {
|
||||
to_list.pop()
|
||||
};
|
||||
|
||||
return Ok(Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: to.map(|s| s.addr),
|
||||
failure,
|
||||
}));
|
||||
}
|
||||
@@ -1792,6 +1774,7 @@ impl MimeMessage {
|
||||
{
|
||||
self.delivery_report = Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: None,
|
||||
failure: true,
|
||||
})
|
||||
}
|
||||
@@ -1848,20 +1831,6 @@ impl MimeMessage {
|
||||
};
|
||||
Ok(parent_timestamp)
|
||||
}
|
||||
|
||||
/// Returns parsed `Chat-Group-Member-Timestamps` header contents.
|
||||
///
|
||||
/// Returns `None` if there is no such header.
|
||||
pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
|
||||
let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
|
||||
self.get_header(HeaderDef::ChatGroupMemberTimestamps)
|
||||
.map(|h| {
|
||||
h.split_ascii_whitespace()
|
||||
.filter_map(|ts| ts.parse::<i64>().ok())
|
||||
.map(|ts| std::cmp::min(now, ts))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
|
||||
@@ -1943,6 +1912,7 @@ pub(crate) struct Report {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DeliveryReport {
|
||||
pub rfc724_mid: String,
|
||||
pub failed_recipient: Option<String>,
|
||||
pub failure: bool,
|
||||
}
|
||||
|
||||
@@ -2308,12 +2278,20 @@ async fn handle_ndn(
|
||||
let msgs: Vec<_> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM msgs
|
||||
WHERE rfc724_mid=? AND from_id=1",
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS msg_id,",
|
||||
" c.id AS chat_id,",
|
||||
" c.type AS type",
|
||||
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
),
|
||||
(&failed.rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
let msg_id: MsgId = row.get("msg_id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let chat_type: Chattype = row.get("type")?;
|
||||
Ok((msg_id, chat_id, chat_type))
|
||||
},
|
||||
|rows| Ok(rows.collect::<Vec<_>>()),
|
||||
)
|
||||
@@ -2321,13 +2299,16 @@ async fn handle_ndn(
|
||||
|
||||
let error = if let Some(error) = error {
|
||||
error
|
||||
} else if let Some(failed_recipient) = &failed.failed_recipient {
|
||||
format!("Delivery to {failed_recipient} failed.").clone()
|
||||
} else {
|
||||
"Delivery to at least one recipient failed.".to_string()
|
||||
};
|
||||
let err_msg = &error;
|
||||
|
||||
let mut first = true;
|
||||
for msg in msgs {
|
||||
let msg_id = msg?;
|
||||
let (msg_id, chat_id, chat_type) = msg?;
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let aggregated_error = message
|
||||
.error
|
||||
@@ -2339,11 +2320,47 @@ async fn handle_ndn(
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
if first {
|
||||
// Add only one info msg for all failed messages
|
||||
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ndn_maybe_add_info_msg(
|
||||
context: &Context,
|
||||
failed: &DeliveryReport,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
match chat_type {
|
||||
Chattype::Group | Chattype::Broadcast => {
|
||||
if let Some(failed_recipient) = &failed.failed_recipient {
|
||||
let contact_id =
|
||||
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
|
||||
.await?
|
||||
.context("contact ID not found")?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
// Tell the user which of the recipients failed if we know that (because in
|
||||
// a group, this might otherwise be unclear)
|
||||
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
|
||||
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
// ndn_maybe_add_info_msg() is about the case when delivery to the group failed.
|
||||
// If we get an NDN for the mailing list, just issue a warning.
|
||||
warn!(context, "ignoring NDN for mailing list.");
|
||||
}
|
||||
Chattype::Single => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailparse::ParsedMail;
|
||||
@@ -3134,7 +3151,11 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
|
||||
// Make sure the file is there even though the html is wrong:
|
||||
let param = &message.parts[0].param;
|
||||
let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap();
|
||||
let blob: BlobObject = param
|
||||
.get_blob(Param::File, &t, false)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
|
||||
let size = f.metadata().await.unwrap().len();
|
||||
assert_eq!(size, 154);
|
||||
@@ -3632,10 +3653,9 @@ On 2020-10-25, Bob wrote:
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mime_modified_large_plain() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let t1 = TestContext::new_alice().await;
|
||||
|
||||
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
|
||||
static REPEAT_CNT: usize = DC_DESIRED_TEXT_LEN / REPEAT_TXT.len() + 2;
|
||||
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
|
||||
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
|
||||
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
|
||||
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
|
||||
@@ -3656,21 +3676,22 @@ On 2020-10-25, Bob wrote:
|
||||
if draft {
|
||||
chat.id.set_draft(&t, Some(&mut msg)).await?;
|
||||
}
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
t.send_msg(chat.id, &mut msg).await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
assert!(msg.has_html());
|
||||
let html = msg.id.get_html(&t).await?.unwrap();
|
||||
assert_eq!(html.matches("<!DOCTYPE html>").count(), 1);
|
||||
assert_eq!(html.matches("just repeated.<br/>").count(), REPEAT_CNT);
|
||||
assert_eq!(
|
||||
msg.id
|
||||
.get_html(&t)
|
||||
.await?
|
||||
.unwrap()
|
||||
.matches("just repeated")
|
||||
.count(),
|
||||
REPEAT_CNT
|
||||
);
|
||||
assert!(
|
||||
msg.text.matches("just repeated.").count()
|
||||
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
|
||||
msg.text.matches("just repeated").count() <= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
|
||||
);
|
||||
assert!(msg.text.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
|
||||
|
||||
let msg = t1.recv_msg(&sent_msg).await;
|
||||
assert!(msg.has_html());
|
||||
assert_eq!(msg.id.get_html(&t1).await?.unwrap(), html);
|
||||
}
|
||||
|
||||
t.set_config(Config::Bot, Some("1")).await?;
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the timestamp of the last successful connection
|
||||
/// Update the timestamp of the last successfull connection
|
||||
/// to the given `host` and `port`
|
||||
/// with the given application protocol `alpn`.
|
||||
///
|
||||
|
||||
300
src/net/http.rs
300
src/net/http.rs
@@ -6,17 +6,14 @@ use http_body_util::BodyExt;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use mime::Mime;
|
||||
use serde::Serialize;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::context::Context;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_rustls;
|
||||
use crate::tools::{create_id, time};
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
/// Response body.
|
||||
pub blob: Vec<u8>,
|
||||
@@ -93,146 +90,9 @@ where
|
||||
Ok(sender)
|
||||
}
|
||||
|
||||
/// Converts the URL to expiration and stale timestamps.
|
||||
fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
|
||||
let now = time();
|
||||
|
||||
let expires = now + 3600 * 24 * 35;
|
||||
let stale = if url.ends_with(".xdc") {
|
||||
// WebXDCs are never stale, they just expire.
|
||||
expires
|
||||
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
|
||||
// Cache images for 1 day.
|
||||
//
|
||||
// As of 2024-12-12 WebXDC icons at <https://webxdc.org/apps/>
|
||||
// use the same path for all app versions,
|
||||
// so may change, but it is not critical if outdated icon is displayed.
|
||||
now + 3600 * 24
|
||||
} else {
|
||||
// Revalidate everything else after 1 hour.
|
||||
//
|
||||
// This includes HTML, CSS and JS.
|
||||
now + 3600
|
||||
};
|
||||
(expires, stale)
|
||||
}
|
||||
|
||||
/// Places the binary into HTTP cache.
|
||||
async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Result<()> {
|
||||
let blob = BlobObject::create(
|
||||
context,
|
||||
&format!("http_cache_{}", create_id()),
|
||||
response.blob.as_slice(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (expires, stale) = http_url_cache_timestamps(url, response.mimetype.as_deref());
|
||||
context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT OR REPLACE INTO http_cache (url, expires, stale, blobname, mimetype, encoding)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
url,
|
||||
expires,
|
||||
stale,
|
||||
blob.as_name(),
|
||||
response.mimetype.as_deref().unwrap_or_default(),
|
||||
response.encoding.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the binary from HTTP cache.
|
||||
///
|
||||
/// Also returns if the response is stale and should be revalidated in the background.
|
||||
async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> {
|
||||
let now = time();
|
||||
let Some((blob_name, mimetype, encoding, stale_timestamp)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT blobname, mimetype, encoding, stale
|
||||
FROM http_cache WHERE url=? AND expires > ?",
|
||||
(url, now),
|
||||
|row| {
|
||||
let blob_name: String = row.get(0)?;
|
||||
let mimetype: Option<String> = Some(row.get(1)?).filter(|s: &String| !s.is_empty());
|
||||
let encoding: Option<String> = Some(row.get(2)?).filter(|s: &String| !s.is_empty());
|
||||
let stale_timestamp: i64 = row.get(3)?;
|
||||
Ok((blob_name, mimetype, encoding, stale_timestamp))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let is_stale = now > stale_timestamp;
|
||||
|
||||
let blob_object = BlobObject::from_name(context, blob_name)?;
|
||||
let blob_abs_path = blob_object.to_abs_path();
|
||||
let blob = match fs::read(blob_abs_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read blob for {url:?} cache entry."))
|
||||
{
|
||||
Ok(blob) => blob,
|
||||
Err(err) => {
|
||||
// This should not happen, but user may go into the blobdir and remove files,
|
||||
// antivirus may delete the file or there may be a bug in housekeeping.
|
||||
warn!(context, "{err:?}.");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let (expires, _stale) = http_url_cache_timestamps(url, mimetype.as_deref());
|
||||
let response = Response {
|
||||
blob,
|
||||
mimetype,
|
||||
encoding,
|
||||
};
|
||||
|
||||
// Update expiration timestamp
|
||||
// to prevent deletion of the file still in use.
|
||||
//
|
||||
// If the response is stale, the caller should revalidate it in the background, so update
|
||||
// `stale` timestamp to avoid revalidating too frequently (and have many parallel revalidation
|
||||
// tasks) if revalidation fails or the HTTP request takes some time. The stale period >= 1 hour,
|
||||
// so 1 more minute won't be a problem.
|
||||
let stale_timestamp = if is_stale { now + 60 } else { stale_timestamp };
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE http_cache SET expires=?, stale=? WHERE url=?",
|
||||
(expires, stale_timestamp, url),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some((response, is_stale)))
|
||||
}
|
||||
|
||||
/// Removes expired cache entries.
|
||||
pub(crate) async fn http_cache_cleanup(context: &Context) -> Result<()> {
|
||||
// Remove cache entries that are already expired
|
||||
// or entries that will not expire in a year
|
||||
// to make sure we don't have invalid timestamps that are way forward in the future.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM http_cache
|
||||
WHERE ?1 > expires OR expires > ?1 + 31536000",
|
||||
(time(),),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetches URL and updates the cache.
|
||||
///
|
||||
/// URL is fetched regardless of whether there is an existing result in the cache.
|
||||
async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
|
||||
let mut url = original_url.to_string();
|
||||
/// Retrieves the binary contents of URL using HTTP GET request.
|
||||
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
|
||||
let mut url = url.to_string();
|
||||
|
||||
// Follow up to 10 http-redirects
|
||||
for _i in 0..10 {
|
||||
@@ -279,42 +139,16 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
|
||||
});
|
||||
let body = response.collect().await?.to_bytes();
|
||||
let blob: Vec<u8> = body.to_vec();
|
||||
let response = Response {
|
||||
return Ok(Response {
|
||||
blob,
|
||||
mimetype,
|
||||
encoding,
|
||||
};
|
||||
info!(context, "Inserting {original_url:?} into cache.");
|
||||
http_cache_put(context, &url, &response).await?;
|
||||
return Ok(response);
|
||||
});
|
||||
}
|
||||
|
||||
Err(anyhow!("Followed 10 redirections"))
|
||||
}
|
||||
|
||||
/// Retrieves the binary contents of URL using HTTP GET request.
|
||||
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
|
||||
if let Some((response, is_stale)) = http_cache_get(context, url).await? {
|
||||
info!(context, "Returning {url:?} from cache.");
|
||||
if is_stale {
|
||||
let context = context.clone();
|
||||
let url = url.to_string();
|
||||
tokio::spawn(async move {
|
||||
// Fetch URL in background to update the cache.
|
||||
info!(context, "Fetching stale {url:?} in background.");
|
||||
if let Err(err) = fetch_url(&context, &url).await {
|
||||
warn!(context, "Failed to revalidate {url:?}: {err:#}.");
|
||||
}
|
||||
});
|
||||
}
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
info!(context, "Not found {url:?} in cache, fetching.");
|
||||
let response = fetch_url(context, url).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Sends an empty POST request to the URL.
|
||||
///
|
||||
/// Returns response text and whether request was successful or not.
|
||||
@@ -407,125 +241,3 @@ pub(crate) async fn post_form<T: Serialize + ?Sized>(
|
||||
let bytes = response.collect().await?.to_bytes();
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_http_cache() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
assert_eq!(http_cache_get(t, "https://webxdc.org/").await?, None);
|
||||
|
||||
let html_response = Response {
|
||||
blob: b"<!DOCTYPE html> ...".to_vec(),
|
||||
mimetype: Some("text/html".to_string()),
|
||||
encoding: None,
|
||||
};
|
||||
|
||||
let xdc_response = Response {
|
||||
blob: b"PK...".to_vec(),
|
||||
mimetype: Some("application/octet-stream".to_string()),
|
||||
encoding: None,
|
||||
};
|
||||
let xdc_editor_url = "https://apps.testrun.org/webxdc-editor-v3.2.0.xdc";
|
||||
let xdc_pixel_url = "https://apps.testrun.org/webxdc-pixel-v2.xdc";
|
||||
|
||||
http_cache_put(t, "https://webxdc.org/", &html_response).await?;
|
||||
|
||||
assert_eq!(http_cache_get(t, xdc_editor_url).await?, None);
|
||||
assert_eq!(http_cache_get(t, xdc_pixel_url).await?, None);
|
||||
assert_eq!(
|
||||
http_cache_get(t, "https://webxdc.org/").await?,
|
||||
Some((html_response.clone(), false))
|
||||
);
|
||||
|
||||
http_cache_put(t, xdc_editor_url, &xdc_response).await?;
|
||||
http_cache_put(t, xdc_pixel_url, &xdc_response).await?;
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url).await?,
|
||||
Some((xdc_response.clone(), false))
|
||||
);
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_pixel_url).await?,
|
||||
Some((xdc_response.clone(), false))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
http_cache_get(t, "https://webxdc.org/").await?,
|
||||
Some((html_response.clone(), false))
|
||||
);
|
||||
|
||||
// HTML is stale after 1 hour, but .xdc is not.
|
||||
SystemTime::shift(Duration::from_secs(3600 + 100));
|
||||
assert_eq!(
|
||||
http_cache_get(t, "https://webxdc.org/").await?,
|
||||
Some((html_response.clone(), true))
|
||||
);
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url).await?,
|
||||
Some((xdc_response.clone(), false))
|
||||
);
|
||||
|
||||
// Stale cache entry can be renewed
|
||||
// even before housekeeping removes old one.
|
||||
http_cache_put(t, "https://webxdc.org/", &html_response).await?;
|
||||
assert_eq!(
|
||||
http_cache_get(t, "https://webxdc.org/").await?,
|
||||
Some((html_response.clone(), false))
|
||||
);
|
||||
|
||||
// 35 days later pixel .xdc expires because we did not request it for 35 days and 1 hour.
|
||||
// But editor is still there because we did not request it for just 35 days.
|
||||
// We have not renewed the editor however, so it becomes stale.
|
||||
SystemTime::shift(Duration::from_secs(3600 * 24 * 35 - 100));
|
||||
|
||||
// Run housekeeping to test that it does not delete the blob too early.
|
||||
housekeeping(t).await?;
|
||||
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url).await?,
|
||||
Some((xdc_response.clone(), true))
|
||||
);
|
||||
assert_eq!(http_cache_get(t, xdc_pixel_url).await?, None);
|
||||
|
||||
// If we get the blob the second time quickly, it shouldn't be stale because it's supposed
|
||||
// that we've already run a revalidation task which will update the blob soon.
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url).await?,
|
||||
Some((xdc_response.clone(), false))
|
||||
);
|
||||
// But if the revalidation task hasn't succeeded after some time, the blob is stale again
|
||||
// even if we continue to get it frequently.
|
||||
for i in (0..100).rev() {
|
||||
SystemTime::shift(Duration::from_secs(6));
|
||||
if let Some((_, true)) = http_cache_get(t, xdc_editor_url).await? {
|
||||
break;
|
||||
}
|
||||
assert!(i > 0);
|
||||
}
|
||||
|
||||
// Test that if the file is accidentally removed from the blobdir,
|
||||
// there is no error when trying to load the cache entry.
|
||||
for entry in std::fs::read_dir(t.get_blobdir())? {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
std::fs::remove_file(path).expect("Failed to remove blob");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
http_cache_get(t, xdc_editor_url)
|
||||
.await
|
||||
.context("Failed to get no cache response")?,
|
||||
None
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ where
|
||||
.get(9..12)
|
||||
.context("HTTP status line does not contain a status code")?;
|
||||
|
||||
// Interpret status code according to
|
||||
// Interpert status code according to
|
||||
// <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
|
||||
if status_code == b"407" {
|
||||
Err(format_err!("Proxy Authentication Required"))
|
||||
@@ -640,9 +640,6 @@ mod tests {
|
||||
fn test_invalid_proxy_url() {
|
||||
assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
|
||||
assert!(ProxyConfig::from_url("abc").is_err());
|
||||
|
||||
// This caused panic before shadowsocks 1.22.0.
|
||||
assert!(ProxyConfig::from_url("ss://foo:bar@127.0.0.1:9999").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
26
src/param.rs
26
src/param.rs
@@ -55,8 +55,6 @@ pub enum Param {
|
||||
|
||||
/// For Messages: decrypted with validation errors or without mutual set, if neither
|
||||
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
|
||||
///
|
||||
/// Deprecated on 2024-12-25.
|
||||
ErroneousE2ee = b'e',
|
||||
|
||||
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
|
||||
@@ -183,8 +181,6 @@ pub enum Param {
|
||||
GroupNameTimestamp = b'g',
|
||||
|
||||
/// For Chats: timestamp of member list update.
|
||||
///
|
||||
/// Deprecated 2025-01-07.
|
||||
MemberListTimestamp = b'k',
|
||||
|
||||
/// For Webxdc Message Instances: Current document name
|
||||
@@ -370,16 +366,20 @@ impl Params {
|
||||
///
|
||||
/// This parses the parameter value as a [ParamsFile] and than
|
||||
/// tries to return a [BlobObject] for that file. If the file is
|
||||
/// not yet a valid blob, one will be created by copying the file.
|
||||
/// not yet a valid blob, one will be created by copying the file
|
||||
/// only if `create` is set to `true`, otherwise an error is
|
||||
/// returned.
|
||||
///
|
||||
/// Note that in the [ParamsFile::FsPath] case the blob can be
|
||||
/// created without copying if the path already refers to a valid
|
||||
/// blob. If so a [BlobObject] will be returned.
|
||||
/// blob. If so a [BlobObject] will be returned regardless of the
|
||||
/// `create` argument.
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
pub async fn get_blob<'a>(
|
||||
&self,
|
||||
key: Param,
|
||||
context: &'a Context,
|
||||
create: bool,
|
||||
) -> Result<Option<BlobObject<'a>>> {
|
||||
let val = match self.get(key) {
|
||||
Some(val) => val,
|
||||
@@ -387,7 +387,10 @@ impl Params {
|
||||
};
|
||||
let file = ParamsFile::from_param(context, val)?;
|
||||
let blob = match file {
|
||||
ParamsFile::FsPath(path) => BlobObject::new_from_path(context, &path).await?,
|
||||
ParamsFile::FsPath(path) => match create {
|
||||
true => BlobObject::new_from_path(context, &path).await?,
|
||||
false => BlobObject::from_path(context, &path)?,
|
||||
},
|
||||
ParamsFile::Blob(blob) => blob,
|
||||
};
|
||||
Ok(Some(blob))
|
||||
@@ -543,20 +546,23 @@ mod tests {
|
||||
let path: PathBuf = p.get_path(Param::File, &t).unwrap().unwrap();
|
||||
assert_eq!(path, fname);
|
||||
|
||||
// Blob does not exist yet, expect error.
|
||||
assert!(p.get_blob(Param::File, &t, false).await.is_err());
|
||||
|
||||
fs::write(fname, b"boo").await.unwrap();
|
||||
let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap();
|
||||
let blob = p.get_blob(Param::File, &t, true).await.unwrap().unwrap();
|
||||
assert!(blob.as_file_name().starts_with("foo"));
|
||||
|
||||
// Blob in blobdir, expect blob.
|
||||
let bar_path = t.get_blobdir().join("bar");
|
||||
p.set(Param::File, bar_path.to_str().unwrap());
|
||||
let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap();
|
||||
let blob = p.get_blob(Param::File, &t, false).await.unwrap().unwrap();
|
||||
assert_eq!(blob, BlobObject::from_name(&t, "bar".to_string()).unwrap());
|
||||
|
||||
p.remove(Param::File);
|
||||
assert!(p.get_file(Param::File, &t).unwrap().is_none());
|
||||
assert!(p.get_path(Param::File, &t).unwrap().is_none());
|
||||
assert!(p.get_blob(Param::File, &t).await.unwrap().is_none());
|
||||
assert!(p.get_blob(Param::File, &t, false).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
//! when it's not required. Only when a webxdc subscribes to realtime data or when a reatlime message is sent,
|
||||
//! the p2p machinery should be started.
|
||||
//!
|
||||
//! Adding peer channels to webxdc needs upfront negotiation of a topic and sharing of public keys so that
|
||||
//! Adding peer channels to webxdc needs upfront negotation of a topic and sharing of public keys so that
|
||||
//! nodes can connect to each other. The explicit approach is as follows:
|
||||
//!
|
||||
//! 1. We introduce a new [`IrohGossipTopic`](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
|
||||
//! 1. We introduce a new [GossipTopic](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
|
||||
//! securely generated on the initial webxdc sender's device. This message header is encrypted
|
||||
//! and sent in the same message as the webxdc application.
|
||||
//! 2. Whenever `joinRealtimeChannel().setListener()` or `joinRealtimeChannel().send()` is called by the webxdc application,
|
||||
//! we start a routine to establish p2p connectivity and join the gossip swarm with Iroh.
|
||||
//! 3. The first step of this routine is to introduce yourself with a regular message containing the [`IrohNodeAddr`](crate::headerdef::HeaderDef::IrohNodeAddr).
|
||||
//! 3. The first step of this routine is to introduce yourself with a regular message containing the `IrohPublicKey`.
|
||||
//! This message contains the users relay-server and public key.
|
||||
//! Direct IP address is not included as this information can be persisted by email providers.
|
||||
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
|
||||
@@ -78,14 +78,6 @@ impl Iroh {
|
||||
self.endpoint.network_change().await
|
||||
}
|
||||
|
||||
/// Closes the QUIC endpoint.
|
||||
pub(crate) async fn close(self) -> Result<()> {
|
||||
self.endpoint
|
||||
.close(0u32.into(), b"")
|
||||
.await
|
||||
.context("Closing iroh endpoint failed")
|
||||
}
|
||||
|
||||
/// Join a topic and create the subscriber loop for it.
|
||||
///
|
||||
/// If there is no gossip, create it.
|
||||
@@ -293,36 +285,15 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Get or initialize the iroh peer channel.
|
||||
pub async fn get_or_try_init_peer_channel(
|
||||
&self,
|
||||
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
|
||||
pub async fn get_or_try_init_peer_channel(&self) -> Result<&Iroh> {
|
||||
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
bail!("Attempt to get Iroh when realtime is disabled");
|
||||
}
|
||||
|
||||
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
|
||||
self.iroh.read().await,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
return Ok(lock);
|
||||
}
|
||||
|
||||
let lock = self.iroh.write().await;
|
||||
match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
) {
|
||||
Ok(lock) => Ok(lock),
|
||||
Err(mut lock) => {
|
||||
let iroh = self.init_peer_channels().await?;
|
||||
*lock = Some(iroh);
|
||||
tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
|
||||
lock,
|
||||
|opt_iroh| opt_iroh.as_ref(),
|
||||
)
|
||||
.map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
|
||||
}
|
||||
}
|
||||
let ctx = self.clone();
|
||||
self.iroh
|
||||
.get_or_try_init(|| async { ctx.init_peer_channels().await })
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +388,7 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeA
|
||||
))
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -654,6 +626,7 @@ mod tests {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
|
||||
@@ -663,23 +636,13 @@ mod tests {
|
||||
.map(|addr| addr.node_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
|
||||
);
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -688,10 +651,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
alice_iroh
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -710,9 +670,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
// Bob sends ephemeral message
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -741,20 +699,10 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
vec![bob_iroh.get_node_addr().await.unwrap().node_id]
|
||||
);
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -772,12 +720,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calling stop_io() closes iroh endpoint as well,
|
||||
// even though I/O was not started in this test.
|
||||
assert!(alice.iroh.read().await.is_some());
|
||||
alice.stop_io().await;
|
||||
assert!(alice.iroh.read().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -819,6 +761,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
|
||||
@@ -828,23 +771,13 @@ mod tests {
|
||||
.map(|addr| addr.node_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alice_iroh = alice.get_or_try_init_peer_channel().await.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
.get_node_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
.node_id
|
||||
]
|
||||
vec![alice_iroh.get_node_addr().await.unwrap().node_id]
|
||||
);
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -853,10 +786,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
alice
|
||||
.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
alice_iroh
|
||||
.send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -881,9 +811,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let bob_sequence_number = bob
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -892,9 +820,7 @@ mod tests {
|
||||
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
|
||||
let bob_sequence_number_after = bob
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.sequence_numbers
|
||||
.lock()
|
||||
@@ -903,9 +829,7 @@ mod tests {
|
||||
// Check that sequence number is persisted when leaving the channel.
|
||||
assert_eq!(bob_sequence_number, bob_sequence_number_after);
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.join_and_subscribe_gossip(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -913,9 +837,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
bob.get_or_try_init_peer_channel()
|
||||
.await
|
||||
.unwrap()
|
||||
bob_iroh
|
||||
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -934,20 +856,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// channel is only used to remember if an advertisement has been sent
|
||||
// channel is only used to remeber if an advertisement has been sent
|
||||
// bob for example does not change the channels because he never sends an
|
||||
// advertisement
|
||||
assert_eq!(
|
||||
alice
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
.await
|
||||
.len(),
|
||||
alice.iroh.get().unwrap().iroh_channels.read().await.len(),
|
||||
1
|
||||
);
|
||||
leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
|
||||
@@ -957,9 +870,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(alice
|
||||
.iroh
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.get()
|
||||
.unwrap()
|
||||
.iroh_channels
|
||||
.read()
|
||||
@@ -1052,19 +963,19 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// This internal function should return error
|
||||
// if accidentally called with the setting disabled.
|
||||
|
||||
@@ -711,25 +711,9 @@ impl Peerstate {
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"UPDATE chats_contacts
|
||||
SET remove_timestamp=MAX(add_timestamp+1, ?)
|
||||
WHERE chat_id=? AND contact_id=?",
|
||||
(timestamp, chat_id, contact_id),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts
|
||||
(chat_id, contact_id, add_timestamp)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT (chat_id, contact_id)
|
||||
DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
|
||||
(chat_id, new_contact_id, timestamp),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id)
|
||||
.await?;
|
||||
chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id])
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(*chat_id));
|
||||
|
||||
117
src/push.rs
117
src/push.rs
@@ -1,24 +1,10 @@
|
||||
//! # Push notifications module.
|
||||
//!
|
||||
//! This module is responsible for Apple Push Notification Service
|
||||
//! and Firebase Cloud Messaging push notifications.
|
||||
//!
|
||||
//! It provides [`PushSubscriber`] type
|
||||
//! which holds push notification token for the device,
|
||||
//! shared by all accounts.
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use pgp::crypto::aead::AeadAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::ser::Serialize;
|
||||
use rand::thread_rng;
|
||||
use anyhow::Result;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
|
||||
/// Manages subscription to Apple Push Notification services.
|
||||
///
|
||||
@@ -38,85 +24,20 @@ pub struct PushSubscriber {
|
||||
inner: Arc<RwLock<PushSubscriberState>>,
|
||||
}
|
||||
|
||||
/// The key was generated with
|
||||
/// `rsop generate-key --profile rfc9580`
|
||||
/// and public key was extracted with `rsop extract-cert`.
|
||||
const NOTIFIERS_PUBLIC_KEY: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xioGZ03cdhsAAAAg6PasQQylEuWAp9N5PXN93rqjZdqOqN3s9RJEU/K8FZzCsAYf
|
||||
GwoAAABBBQJnTdx2AhsDAh4JCAsJCAcKDQwLBRUKCQgLAhYCIiEGiJJktnCmEtXa
|
||||
qsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAAUfgg/sg0sR2mytzADFBpNAaY0Hyu
|
||||
aru8ics3eUkeNn2ziL4ZsIMx+4mcM5POvD0PG9LtH8Rz/y9iItD0c2aoRBab7iri
|
||||
/gDm6aQuj3xXgtAiXdaN9s+QPxR9gY/zG1t9iXgBzioGZ03cdhkAAAAgwJ0wQFsk
|
||||
MGH4jklfK1fFhYoQZMjEFCRBIk+r1S+WaSDClQYYGwgAAAAsBQJnTdx2AhsMIiEG
|
||||
iJJktnCmEtXaqsSIGRJtupMnxycz/yT0xZK9ez+YkmIAAAAKCRCIkmS2cKYS1WdP
|
||||
EFerccH2BoIPNbrxi6hwvxxy7G1mHg//ofD90fqmeY9xTfKMYl16bqQh4R1PiYd5
|
||||
LMc5VqgXHgioqTYKbltlOtWC+HDt/PrymQsN4q/aEmsM
|
||||
=5jvt
|
||||
-----END PGP PUBLIC KEY BLOCK-----";
|
||||
|
||||
/// Pads the token with spaces.
|
||||
///
|
||||
/// This makes it impossible to tell
|
||||
/// if the user is an Apple user with shorter tokens
|
||||
/// or FCM user with longer tokens by the length of ciphertext.
|
||||
fn pad_device_token(s: &str) -> String {
|
||||
// 512 is larger than any token, tokens seen so far have not been larger than 200 bytes.
|
||||
let expected_len: usize = 512;
|
||||
let payload_len = s.len();
|
||||
let padding_len = expected_len.saturating_sub(payload_len);
|
||||
let padding = " ".repeat(padding_len);
|
||||
let res = format!("{s}{padding}");
|
||||
debug_assert_eq!(res.len(), expected_len);
|
||||
res
|
||||
}
|
||||
|
||||
/// Encrypts device token with OpenPGP.
|
||||
///
|
||||
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
|
||||
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
|
||||
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
|
||||
let encryption_subkey = public_key
|
||||
.public_subkeys
|
||||
.first()
|
||||
.context("No encryption subkey found")?;
|
||||
let padded_device_token = pad_device_token(device_token);
|
||||
let literal_message = pgp::composed::Message::new_literal("", &padded_device_token);
|
||||
let mut rng = thread_rng();
|
||||
let chunk_size = 8;
|
||||
|
||||
let encrypted_message = literal_message.encrypt_to_keys_seipdv2(
|
||||
&mut rng,
|
||||
SymmetricKeyAlgorithm::AES128,
|
||||
AeadAlgorithm::Ocb,
|
||||
chunk_size,
|
||||
&[&encryption_subkey],
|
||||
)?;
|
||||
let encoded_message = encrypted_message.to_bytes()?;
|
||||
Ok(format!(
|
||||
"openpgp:{}",
|
||||
base64::engine::general_purpose::STANDARD.encode(encoded_message)
|
||||
))
|
||||
}
|
||||
|
||||
impl PushSubscriber {
|
||||
/// Creates new push notification subscriber.
|
||||
pub(crate) fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Sets device token for Apple Push Notification service
|
||||
/// or Firebase Cloud Messaging.
|
||||
/// Sets device token for Apple Push Notification service.
|
||||
pub(crate) async fn set_device_token(&self, token: &str) {
|
||||
self.inner.write().await.device_token = Some(token.to_string());
|
||||
}
|
||||
|
||||
/// Retrieves device token.
|
||||
///
|
||||
/// The token is encrypted with OpenPGP.
|
||||
///
|
||||
/// Token may be not available if application is not running on Apple platform,
|
||||
/// does not have Google Play services,
|
||||
/// failed to register for remote notifications or is in the process of registering.
|
||||
///
|
||||
/// IMAP loop should periodically check if device token is available
|
||||
@@ -200,37 +121,3 @@ impl Context {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_device_token() {
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
assert_eq!(push_subscriber.device_token().await, None);
|
||||
|
||||
push_subscriber.set_device_token("some-token").await;
|
||||
let device_token = push_subscriber.device_token().await.unwrap();
|
||||
assert_eq!(device_token, "some-token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pad_device_token() {
|
||||
let apple_token = "0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894";
|
||||
assert_eq!(pad_device_token(apple_token).trim(), apple_token);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_device_token() {
|
||||
let fcm_token = encrypt_device_token("fcm-chat.delta:c67DVcpVQN2rJHiSszKNDW:APA91bErcJV2b8qG0IT4aiuCqw6Al0_SbydSuz3V0CHBR1X7Fp8YzyvlpxNZIOGYVDFKejZGE1YiGSaqxmkr9ds0DuALmZNDwqIhuZWGKKrs3r7DTSkQ9MQ").unwrap();
|
||||
let fcm_beta_token = encrypt_device_token("fcm-chat.delta.beta:chu-GhZCTLyzq1XseJp3na:APA91bFlsfDawdszWTyOLbxBy7KeRCrYM-SBFqutebF5ix0EZKMuCFUT_Y7R7Ex_eTQG_LbOu3Ky_z5UlTMJtI7ufpIp5wEvsFmVzQcOo3YhrUpbiSVGIlk").unwrap();
|
||||
let apple_token = encrypt_device_token(
|
||||
"0155b93b7eb867a0d8b7328b978bb15bf22f70867e39e168d03f199af9496894",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fcm_token.len(), fcm_beta_token.len());
|
||||
assert_eq!(apple_token.len(), fcm_token.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Internet Message Format reception pipeline.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -15,7 +14,7 @@ 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::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;
|
||||
@@ -26,16 +25,17 @@ use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_ex, Message, MessageState, MessengerMessage, MsgId,
|
||||
Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::rusqlite::OptionalExtension;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::simplify;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||
@@ -73,7 +73,6 @@ pub struct ReceivedMsg {
|
||||
///
|
||||
/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
|
||||
/// for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub async fn receive_imf(
|
||||
context: &Context,
|
||||
imf_raw: &[u8],
|
||||
@@ -106,7 +105,6 @@ pub async fn receive_imf(
|
||||
/// Emulates reception of a message from "INBOX".
|
||||
///
|
||||
/// Only used for tests and REPL tool, not actual message reception pipeline.
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub(crate) async fn receive_imf_from_inbox(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
@@ -345,18 +343,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let past_ids = add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.past_members,
|
||||
if !mime_parser.incoming {
|
||||
Origin::OutgoingTo
|
||||
} else if incoming_origin.is_known() {
|
||||
Origin::IncomingTo
|
||||
} else {
|
||||
Origin::IncomingUnknownTo
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_verified_keys(context, &mut mime_parser, from_id).await?;
|
||||
|
||||
@@ -372,7 +358,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
} else {
|
||||
let to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
|
||||
let to_id = to_ids.first().copied().unwrap_or_default();
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
|
||||
.await
|
||||
@@ -430,7 +416,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
&mut mime_parser,
|
||||
imf_raw,
|
||||
&to_ids,
|
||||
&past_ids,
|
||||
rfc724_mid_orig,
|
||||
from_id,
|
||||
seen,
|
||||
@@ -453,10 +438,10 @@ pub(crate) async fn receive_imf_inner(
|
||||
// and waste traffic.
|
||||
let chat_id = received_msg.chat_id;
|
||||
if !chat_id.is_special()
|
||||
&& mime_parser.recipients.iter().all(|recipient| {
|
||||
recipient.addr == mime_parser.from.addr
|
||||
|| mime_parser.gossiped_keys.contains_key(&recipient.addr)
|
||||
})
|
||||
&& mime_parser
|
||||
.recipients
|
||||
.iter()
|
||||
.all(|recipient| mime_parser.gossiped_keys.contains_key(&recipient.addr))
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
@@ -620,7 +605,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
if let Some(replace_chat_id) = replace_chat_id {
|
||||
context.emit_msgs_changed_without_msg_id(replace_chat_id);
|
||||
context.emit_msgs_changed(replace_chat_id, MsgId::new(0));
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh;
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
@@ -634,11 +619,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
.await;
|
||||
|
||||
if let Some(is_bot) = mime_parser.is_bot {
|
||||
// If the message is auto-generated and was generated by Delta Chat,
|
||||
// mark the contact as a bot.
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
|
||||
from_id.mark_bot(context, is_bot).await?;
|
||||
}
|
||||
from_id.mark_bot(context, is_bot).await?;
|
||||
}
|
||||
|
||||
Ok(Some(received_msg))
|
||||
@@ -702,7 +683,6 @@ async fn add_parts(
|
||||
mime_parser: &mut MimeMessage,
|
||||
imf_raw: &[u8],
|
||||
to_ids: &[ContactId],
|
||||
past_ids: &[ContactId],
|
||||
rfc724_mid: &str,
|
||||
from_id: ContactId,
|
||||
seen: bool,
|
||||
@@ -792,11 +772,6 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() && is_mdn {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
if mime_parser.incoming {
|
||||
to_id = ContactId::SELF;
|
||||
|
||||
@@ -808,6 +783,11 @@ async fn add_parts(
|
||||
markseen_on_imap_table(context, rfc724_mid).await.ok();
|
||||
}
|
||||
|
||||
if chat_id.is_none() && is_mdn {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
let create_blocked_default = if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
@@ -850,7 +830,6 @@ async fn add_parts(
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
&verified_encryption,
|
||||
&grpid,
|
||||
)
|
||||
@@ -921,7 +900,7 @@ async fn add_parts(
|
||||
group_chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
is_partial_download.is_some(),
|
||||
&verified_encryption,
|
||||
)
|
||||
.await?;
|
||||
@@ -961,11 +940,14 @@ async fn add_parts(
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
} else if allow_creation {
|
||||
let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
|
||||
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
|
||||
.await
|
||||
.context("Failed to get (new) chat for contact")?;
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
.context("Failed to get (new) chat for contact")
|
||||
.log_err(context)
|
||||
{
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
@@ -989,6 +971,7 @@ async fn add_parts(
|
||||
// the 1:1 chat accordingly.
|
||||
let chat = match is_partial_download.is_none()
|
||||
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
|
||||
&& !is_mdn
|
||||
{
|
||||
true => Some(Chat::load_from_db(context, chat_id).await?)
|
||||
.filter(|chat| chat.typ == Chattype::Single),
|
||||
@@ -1049,13 +1032,9 @@ async fn add_parts(
|
||||
// the mail is on the IMAP server, probably it is also delivered.
|
||||
// We cannot recreate other states (read, error).
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
|
||||
to_id = to_ids.first().copied().unwrap_or_default();
|
||||
|
||||
// Older Delta Chat versions with core <=1.152.2 only accepted
|
||||
// self-sent messages in Saved Messages with own address in the `To` field.
|
||||
// New Delta Chat versions may use empty `To` field
|
||||
// with only a single `hidden-recipients` group in this case.
|
||||
let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
|
||||
let self_sent = to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
|
||||
|
||||
if mime_parser.sync_items.is_some() && self_sent {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -1090,7 +1069,6 @@ async fn add_parts(
|
||||
Blocked::Not,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
&verified_encryption,
|
||||
&grpid,
|
||||
)
|
||||
@@ -1162,8 +1140,9 @@ async fn add_parts(
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
}
|
||||
} else {
|
||||
let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
|
||||
} else if let Ok(chat) =
|
||||
ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await
|
||||
{
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
@@ -1179,7 +1158,7 @@ async fn add_parts(
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1191,12 +1170,32 @@ async fn add_parts(
|
||||
chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
past_ids,
|
||||
is_partial_download.is_some(),
|
||||
&verified_encryption,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
// from_id==to_id==ContactId::SELF - this is a self-sent messages,
|
||||
// maybe an Autocrypt Setup Message
|
||||
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
|
||||
.await
|
||||
.context("Failed to get (new) chat for contact")
|
||||
.log_err(context)
|
||||
{
|
||||
chat_id = Some(chat.id);
|
||||
chat_id_blocked = chat.blocked;
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if Blocked::Not != chat_id_blocked {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// Check if the message belongs to a broadcast list.
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||
@@ -1212,21 +1211,6 @@ async fn add_parts(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
// from_id==to_id==ContactId::SELF - this is a self-sent messages,
|
||||
// maybe an Autocrypt Setup Message
|
||||
let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
|
||||
.await
|
||||
.context("Failed to get (new) chat for contact")?;
|
||||
|
||||
chat_id = Some(chat.id);
|
||||
// Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning.
|
||||
|
||||
if Blocked::Not != chat.blocked {
|
||||
chat.id.unblock_ex(context, Nosync).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fetching_existing_messages && mime_parser.decrypting_failed {
|
||||
@@ -1246,7 +1230,7 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
let orig_chat_id = chat_id;
|
||||
let mut chat_id = if is_reaction {
|
||||
let mut chat_id = if is_mdn || is_reaction {
|
||||
DC_CHAT_ID_TRASH
|
||||
} else {
|
||||
chat_id.unwrap_or_else(|| {
|
||||
@@ -1370,7 +1354,7 @@ async fn add_parts(
|
||||
// 1. They can't be an attack (they are outgoing, not incoming)
|
||||
// 2. Probably the unencryptedness is just a temporary state, after all
|
||||
// the user obviously still uses DC
|
||||
// -> Showing info messages every time would be a lot of noise
|
||||
// -> Showing info messages everytime would be a lot of noise
|
||||
// 3. The info messages that are shown to the user ("Your chat partner
|
||||
// likely reinstalled DC" or similar) would be wrong.
|
||||
if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
|
||||
@@ -1420,11 +1404,10 @@ async fn add_parts(
|
||||
// we save the full mime-message and add a flag
|
||||
// that the ui should show button to display the full message.
|
||||
|
||||
// We add "Show Full Message" button to the last message bubble (part) if this flag evaluates to
|
||||
// `true` finally.
|
||||
let mut save_mime_modified = false;
|
||||
// a flag used to avoid adding "show full message" button to multiple parts of the message.
|
||||
let mut save_mime_modified = mime_parser.is_mime_modified;
|
||||
|
||||
let mime_headers = if save_mime_headers || mime_parser.is_mime_modified {
|
||||
let mime_headers = if save_mime_headers || save_mime_modified {
|
||||
let headers = if !mime_parser.decoded_data.is_empty() {
|
||||
mime_parser.decoded_data.clone()
|
||||
} else {
|
||||
@@ -1490,8 +1473,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
let mut parts = mime_parser.parts.iter().peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
for part in &mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
|
||||
@@ -1535,11 +1517,14 @@ async fn add_parts(
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
};
|
||||
|
||||
let part_is_empty =
|
||||
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
|
||||
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
|
||||
let save_mime_modified = save_mime_modified && parts.peek().is_none();
|
||||
let mime_modified = save_mime_modified && !part_is_empty;
|
||||
if mime_modified {
|
||||
// Avoid setting mime_modified for more than one part.
|
||||
save_mime_modified = false;
|
||||
}
|
||||
|
||||
if part.typ == Viewtype::Text {
|
||||
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
|
||||
@@ -1559,7 +1544,8 @@ async fn add_parts(
|
||||
|
||||
// If you change which information is skipped if the message is trashed,
|
||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||
let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
|
||||
let trash =
|
||||
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -1622,14 +1608,14 @@ RETURNING id
|
||||
},
|
||||
hidden,
|
||||
part.bytes as isize,
|
||||
if (save_mime_headers || save_mime_modified) && !trash {
|
||||
if (save_mime_headers || mime_modified) && !trash {
|
||||
mime_headers.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
save_mime_modified,
|
||||
mime_modified,
|
||||
part.error.as_deref().unwrap_or_default(),
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp,
|
||||
@@ -1704,7 +1690,12 @@ RETURNING id
|
||||
"Message has {icnt} parts and is assigned to chat #{chat_id}."
|
||||
);
|
||||
|
||||
if !chat_id.is_trash() {
|
||||
// new outgoing message from another device marks the chat as noticed.
|
||||
if !mime_parser.incoming && !chat_id.is_special() {
|
||||
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
|
||||
}
|
||||
|
||||
if !is_mdn {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
// In contrast to most other update-timestamps,
|
||||
@@ -1887,44 +1878,34 @@ async fn lookup_chat_or_create_adhoc_group(
|
||||
if !contact_ids.contains(&from_id) {
|
||||
contact_ids.push(from_id);
|
||||
}
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
t.pragma_update(None, "query_only", "0")?;
|
||||
t.execute(
|
||||
"CREATE TEMP TABLE temp.contacts (
|
||||
id INTEGER PRIMARY KEY
|
||||
) STRICT",
|
||||
(),
|
||||
)?;
|
||||
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
|
||||
for &id in &contact_ids {
|
||||
stmt.execute((id,))?;
|
||||
}
|
||||
let val = t
|
||||
.query_row(
|
||||
if let Some((chat_id, blocked)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
&format!(
|
||||
"SELECT c.id, c.blocked
|
||||
FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
|
||||
WHERE m.hidden=0 AND c.grpid='' AND c.name=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts
|
||||
WHERE chat_id=c.id
|
||||
AND add_timestamp >= remove_timestamp)=?
|
||||
WHERE chat_id=c.id)=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts
|
||||
WHERE chat_id=c.id
|
||||
AND contact_id NOT IN (SELECT id FROM temp.contacts)
|
||||
AND add_timestamp >= remove_timestamp)=0
|
||||
WHERE chat_id=c.id
|
||||
AND contact_id NOT IN ({}))=0
|
||||
ORDER BY m.timestamp DESC",
|
||||
(&grpname, contact_ids.len()),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
Ok((id, blocked))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
t.execute("DROP TABLE temp.contacts", ())?;
|
||||
Ok(val)
|
||||
};
|
||||
let query_only = true;
|
||||
if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
|
||||
sql::repeat_vars(contact_ids.len()),
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[&grpname])
|
||||
.chain(params_iter(&[contact_ids.len()]))
|
||||
.chain(params_iter(&contact_ids)),
|
||||
),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
Ok((id, blocked))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to ad-hoc group {chat_id} with matching name and members."
|
||||
@@ -1996,7 +1977,6 @@ async fn create_group(
|
||||
create_blocked: Blocked,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
past_ids: &[ContactId],
|
||||
verified_encryption: &VerifiedEncryption,
|
||||
grpid: &str,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
@@ -2070,37 +2050,14 @@ async fn create_group(
|
||||
chat_id_blocked = create_blocked;
|
||||
|
||||
// Create initial member list.
|
||||
if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
|
||||
let mut new_to_ids = to_ids.to_vec();
|
||||
if !new_to_ids.contains(&from_id) {
|
||||
new_to_ids.insert(0, from_id);
|
||||
chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
|
||||
}
|
||||
|
||||
update_chats_contacts_timestamps(
|
||||
context,
|
||||
new_chat_id,
|
||||
None,
|
||||
&new_to_ids,
|
||||
past_ids,
|
||||
&chat_group_member_timestamps,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let mut members = vec![ContactId::SELF];
|
||||
if !from_id.is_special() {
|
||||
members.push(from_id);
|
||||
}
|
||||
members.extend(to_ids);
|
||||
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
new_chat_id,
|
||||
&members,
|
||||
)
|
||||
.await?;
|
||||
let mut members = vec![ContactId::SELF];
|
||||
if !from_id.is_special() {
|
||||
members.push(from_id);
|
||||
}
|
||||
members.extend(to_ids);
|
||||
members.sort_unstable();
|
||||
members.dedup();
|
||||
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);
|
||||
@@ -2125,86 +2082,13 @@ async fn create_group(
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_chats_contacts_timestamps(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
ignored_id: Option<ContactId>,
|
||||
to_ids: &[ContactId],
|
||||
past_ids: &[ContactId],
|
||||
chat_group_member_timestamps: &[i64],
|
||||
) -> Result<bool> {
|
||||
let expected_timestamps_count = to_ids.len() + past_ids.len();
|
||||
|
||||
if chat_group_member_timestamps.len() != expected_timestamps_count {
|
||||
warn!(
|
||||
context,
|
||||
"Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
|
||||
chat_group_member_timestamps.len(),
|
||||
expected_timestamps_count
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut modified = false;
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut add_statement = transaction.prepare(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT (chat_id, contact_id)
|
||||
DO
|
||||
UPDATE SET add_timestamp=?3
|
||||
WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
|
||||
)?;
|
||||
|
||||
for (contact_id, ts) in iter::zip(
|
||||
to_ids.iter(),
|
||||
chat_group_member_timestamps.iter().take(to_ids.len()),
|
||||
) {
|
||||
if Some(*contact_id) != ignored_id {
|
||||
// It could be that member was already added,
|
||||
// but updated addition timestamp
|
||||
// is also a modification worth notifying about.
|
||||
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut remove_statement = transaction.prepare(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT (chat_id, contact_id)
|
||||
DO
|
||||
UPDATE SET remove_timestamp=?3
|
||||
WHERE ?3>remove_timestamp AND ?3>add_timestamp",
|
||||
)?;
|
||||
|
||||
for (contact_id, ts) in iter::zip(
|
||||
past_ids.iter(),
|
||||
chat_group_member_timestamps.iter().skip(to_ids.len()),
|
||||
) {
|
||||
// It could be that member was already removed,
|
||||
// but updated removal timestamp
|
||||
// is also a modification worth notifying about.
|
||||
modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(modified)
|
||||
}
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
|
||||
/// original system message. If the better message is empty, the original system message
|
||||
/// should be trashed.
|
||||
/// original system message. If the better message is empty, the original system message should be
|
||||
/// just omitted.
|
||||
///
|
||||
/// * `to_ids` - contents of the `To` and `Cc` headers.
|
||||
/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
|
||||
/// * `is_partial_download` - whether the message is not fully downloaded.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
@@ -2212,7 +2096,7 @@ async fn apply_group_changes(
|
||||
chat_id: ChatId,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
past_ids: &[ContactId],
|
||||
is_partial_download: bool,
|
||||
verified_encryption: &VerifiedEncryption,
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
if chat_id.is_special() {
|
||||
@@ -2241,6 +2125,49 @@ 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 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
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
self_added
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists_ex(context, reply_to, "download_state=0")
|
||||
.await?
|
||||
.filter(|(_, _, downloaded)| *downloaded)
|
||||
.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && (
|
||||
// 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
|
||||
}
|
||||
);
|
||||
|
||||
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
|
||||
@@ -2264,24 +2191,46 @@ async fn apply_group_changes(
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
if let Some(id) = removed_id {
|
||||
better_msg = if id == from_id {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
if allow_member_list_changes && chat_contacts.contains(&id) {
|
||||
better_msg = if id == from_id {
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Removed {removed_addr:?} has no contact id.")
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
added_id = Some(contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.");
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
);
|
||||
}
|
||||
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
|
||||
if allow_member_list_changes {
|
||||
let is_new_member;
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
if !recreate_member_list {
|
||||
added_id = Some(contact_id);
|
||||
}
|
||||
is_new_member = !chat_contacts.contains(&contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.");
|
||||
is_new_member = false;
|
||||
}
|
||||
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
if is_new_member || self_added {
|
||||
better_msg =
|
||||
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
}
|
||||
} else {
|
||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||
}
|
||||
better_msg.get_or_insert_with(Default::default);
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.map(|s| s.trim())
|
||||
@@ -2328,114 +2277,80 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
|
||||
// These are for adding info messages about implicit membership changes, so they are only
|
||||
// filled when such messages are needed.
|
||||
let mut added_ids = HashSet::<ContactId>::new();
|
||||
let mut removed_ids = HashSet::<ContactId>::new();
|
||||
if allow_member_list_changes {
|
||||
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
|
||||
if is_from_in_chat {
|
||||
if let Some(ref chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
|
||||
send_event_chat_modified |= update_chats_contacts_timestamps(
|
||||
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
|
||||
// if they removed a recipient then it was probably by accident.
|
||||
// - DC users could miss new member additions and then better to handle this in the same
|
||||
// way as for classical MUA messages. Moreover, if we remove a member implicitly, they
|
||||
// will never know that and continue to think they're still here.
|
||||
// But it shouldn't be a big problem if somebody missed a member removal, because they
|
||||
// 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.
|
||||
new_members.extend(diff.clone());
|
||||
if let Some(added_id) = added_id {
|
||||
diff.remove(&added_id);
|
||||
}
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
|
||||
}
|
||||
group_changes_msgs.reserve(diff.len());
|
||||
for contact_id in diff {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::UNDEFINED,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
if recreate_member_list {
|
||||
info!(
|
||||
context,
|
||||
chat_id,
|
||||
Some(from_id),
|
||||
to_ids,
|
||||
past_ids,
|
||||
chat_group_member_timestamps,
|
||||
)
|
||||
.await?;
|
||||
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
||||
chat::get_chat_contacts(context, chat_id)
|
||||
.await?
|
||||
.iter()
|
||||
.copied(),
|
||||
"Recreating chat {chat_id} member list with {new_members:?}.",
|
||||
);
|
||||
added_ids = new_chat_contacts
|
||||
.difference(&chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
removed_ids = chat_contacts
|
||||
.difference(&new_chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
} else {
|
||||
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
}
|
||||
|
||||
if !self_added {
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
|
||||
// Allow non-Delta Chat MUAs to add members.
|
||||
added_ids = new_members.difference(&chat_contacts).copied().collect();
|
||||
}
|
||||
|
||||
if let Some(added_id) = added_id {
|
||||
added_ids.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:
|
||||
new_members.extend(added_ids.clone());
|
||||
if new_members != chat_contacts {
|
||||
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
|
||||
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;
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
chat::update_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
chat_id,
|
||||
&new_members,
|
||||
)
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, ts)
|
||||
.await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(added_id) = added_id {
|
||||
if !added_ids.remove(&added_id) && !self_added {
|
||||
// No-op "Member added" message.
|
||||
//
|
||||
// Trash it.
|
||||
better_msg = Some(String::new());
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
removed_ids.remove(&removed_id);
|
||||
}
|
||||
if !added_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit addition of {added_ids:?} to chat {chat_id}."
|
||||
);
|
||||
}
|
||||
if !removed_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit removal of {removed_ids:?} from chat {chat_id}."
|
||||
);
|
||||
}
|
||||
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
|
||||
for contact_id in added_ids {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
for contact_id in removed_ids {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
if !chat_contacts.contains(&ContactId::SELF) {
|
||||
warn!(
|
||||
@@ -2542,13 +2457,7 @@ async fn create_or_lookup_mailinglist(
|
||||
)
|
||||
})?;
|
||||
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
chat_id,
|
||||
&[ContactId::SELF],
|
||||
)
|
||||
.await?;
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
|
||||
Ok(Some((chat_id, blocked)))
|
||||
} else {
|
||||
info!(context, "Creating list forbidden by caller.");
|
||||
@@ -2744,13 +2653,7 @@ async fn create_adhoc_group(
|
||||
context,
|
||||
"Created ad-hoc group id={new_chat_id}, name={grpname:?}."
|
||||
);
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
new_chat_id,
|
||||
&member_ids,
|
||||
)
|
||||
.await?;
|
||||
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);
|
||||
@@ -2883,27 +2786,38 @@ async fn mark_recipients_as_verified(
|
||||
to_ids: Vec<ContactId>,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
if to_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
sql::repeat_vars(to_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&to_ids),
|
||||
|row| {
|
||||
let to_addr: String = row.get(0)?;
|
||||
let is_verified: i32 = row.get(1).unwrap_or(0);
|
||||
Ok((to_addr, is_verified != 0))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
for id in to_ids {
|
||||
let Some((to_addr, is_verified)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
|
||||
(id,),
|
||||
|row| {
|
||||
let to_addr: String = row.get(0)?;
|
||||
let is_verified: i32 = row.get(1).unwrap_or(0);
|
||||
Ok((to_addr, is_verified != 0))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for (to_addr, is_verified) in rows {
|
||||
// mark gossiped keys (if any) as verified
|
||||
if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
|
||||
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
|
||||
@@ -3001,12 +2915,14 @@ pub(crate) async fn get_prefetch_parent_message(
|
||||
}
|
||||
|
||||
/// Looks up contact IDs from the database given the list of recipients.
|
||||
///
|
||||
/// Returns vector of IDs guaranteed to be unique.
|
||||
async fn add_or_lookup_contacts_by_address_list(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
origin: Origin,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let mut contact_ids = Vec::new();
|
||||
let mut contact_ids = HashSet::new();
|
||||
for info in address_list {
|
||||
let addr = &info.addr;
|
||||
if !may_be_valid_addr(addr) {
|
||||
@@ -3017,13 +2933,13 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
|
||||
.await?;
|
||||
contact_ids.push(contact_id);
|
||||
contact_ids.insert(contact_id);
|
||||
} else {
|
||||
warn!(context, "Contact with address {:?} cannot exist.", addr);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(contact_ids)
|
||||
Ok(contact_ids.into_iter().collect::<Vec<ContactId>>())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -566,8 +566,6 @@ async fn test_escaped_recipients() {
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
// We test with non-chat message here
|
||||
// because chat messages are not expected to have `Cc` header.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Foobar <foobar@example.com>\n\
|
||||
@@ -575,6 +573,8 @@ async fn test_escaped_recipients() {
|
||||
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
@@ -590,12 +590,11 @@ async fn test_escaped_recipients() {
|
||||
let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
assert_eq!(msg.text, "foo – hello");
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
assert_eq!(msg.text, "hello");
|
||||
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||
}
|
||||
|
||||
/// Tests that `Cc` header updates display name
|
||||
/// if existing contact has low enough origin.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cc_to_contact() {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -613,8 +612,6 @@ async fn test_cc_to_contact() {
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
// We use non-chat message here
|
||||
// because chat messages are not expected to have `Cc` header.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
@@ -623,6 +620,8 @@ async fn test_cc_to_contact() {
|
||||
Cc: Carl <carl@host.tld>\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <asdklfjjaweofi@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
@@ -869,10 +868,18 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
|
||||
let ChatItem::Message { msg_id } = *msgs.last().unwrap() else {
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
assert_eq!(msg_id, msg.id);
|
||||
let last_msg = Message::load_from_db(&t, *msg_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
last_msg.text,
|
||||
stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await
|
||||
);
|
||||
assert_eq!(last_msg.from_id, ContactId::INFO);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2201,30 +2208,6 @@ Message content",
|
||||
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
|
||||
}
|
||||
|
||||
/// Tests that message with hidden recipients is assigned to Saved Messages chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_hidden_recipients_self_chat() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"Subject: s
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <foobar@localhost>
|
||||
To: hidden-recipients:;
|
||||
From: <alice@example.org>
|
||||
|
||||
Message content",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, t.get_self_chat().await.id);
|
||||
assert_eq!(msg.to_id, ContactId::SELF);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_unencrypted_name_in_self_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3334,7 +3317,6 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
|
||||
chat::add_to_chat_contacts_table(
|
||||
&bob,
|
||||
time(),
|
||||
group_id,
|
||||
&[
|
||||
bob.add_or_lookup_contact(&alice1).await.id,
|
||||
@@ -3544,27 +3526,26 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.section("Bob creates a group");
|
||||
// =============== Bob creates a group ===============
|
||||
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
|
||||
chat::add_to_chat_contacts_table(
|
||||
&bob,
|
||||
time(),
|
||||
group_id,
|
||||
&[bob.add_or_lookup_contact(&alice).await.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
tcm.section("Bob sends the first message to the group");
|
||||
// =============== Bob sends the first message to the group ===============
|
||||
let sent = bob.send_text(group_id, "Hello all!").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
tcm.section("Bob blocks Alice");
|
||||
// =============== Bob blocks Alice ================
|
||||
Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
|
||||
|
||||
tcm.section("Alice replies private to Bob");
|
||||
// =============== Alice replies private to Bob ==============
|
||||
let received = alice.get_last_msg().await;
|
||||
assert_eq!(received.text, "Hello all!");
|
||||
|
||||
@@ -3578,7 +3559,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await;
|
||||
bob.recv_msg(&sent2).await;
|
||||
|
||||
// check that no contact request was created
|
||||
// ========= check that no contact request was created ============
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0).unwrap();
|
||||
@@ -3589,7 +3570,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let received = bob.get_last_msg().await;
|
||||
assert_eq!(received.text, "Hello all!");
|
||||
|
||||
tcm.section("Bob unblocks Alice");
|
||||
// =============== Bob unblocks Alice ================
|
||||
// test if the blocked chat is restored correctly
|
||||
Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?;
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap();
|
||||
@@ -3854,61 +3835,6 @@ async fn test_messed_up_message_id() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_big_forwarded_with_big_attachment() -> Result<()> {
|
||||
let t = &TestContext::new_bob().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/big_forwarded_with_big_attachment.eml");
|
||||
let rcvd = receive_imf(t, raw, false).await?.unwrap();
|
||||
assert_eq!(rcvd.msg_ids.len(), 3);
|
||||
|
||||
let msg = Message::load_from_db(t, rcvd.msg_ids[0]).await?;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Text);
|
||||
assert_eq!(msg.get_text(), "Hello!");
|
||||
assert!(!msg.has_html());
|
||||
|
||||
let msg = Message::load_from_db(t, rcvd.msg_ids[1]).await?;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Text);
|
||||
assert!(msg
|
||||
.get_text()
|
||||
.starts_with("this text with 42 chars is just repeated."));
|
||||
assert!(msg.get_text().ends_with("[...]"));
|
||||
assert!(!msg.has_html());
|
||||
|
||||
let msg = Message::load_from_db(t, rcvd.msg_ids[2]).await?;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
assert!(msg.has_html());
|
||||
let html = msg.id.get_html(t).await?.unwrap();
|
||||
let tail = html
|
||||
.split_once("Hello!")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("From: AAA")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("aaa@example.org")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("To: Alice")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("alice@example.org")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("Subject: Some subject")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("Date: Fri, 2 Jun 2023 12:29:17 +0000")
|
||||
.unwrap()
|
||||
.1;
|
||||
assert_eq!(
|
||||
tail.matches("this text with 42 chars is just repeated.")
|
||||
.count(),
|
||||
128
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mua_user_adds_member() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -4107,7 +4033,7 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// re-add bob
|
||||
// readd bob
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let add2 = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&add2).await;
|
||||
@@ -4154,15 +4080,11 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
|
||||
// Bob replies again, even after some time this does not add Alice back.
|
||||
//
|
||||
// Bob cannot learn from Alice that Alice has left the group
|
||||
// because Alice is not going to send more messages to the group.
|
||||
// Bob replies again adding Alice back.
|
||||
send_text_msg(bob, bob_chat_id, "i'm bob".to_string()).await?;
|
||||
let msg = &bob.pop_sent_msg().await;
|
||||
alice.recv_msg(msg).await;
|
||||
|
||||
assert!(!is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
|
||||
assert!(is_contact_in_chat(alice, alice_chat_id, ContactId::SELF).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4223,7 +4145,7 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delayed_removal_is_ignored() -> Result<()> {
|
||||
async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
@@ -4231,7 +4153,6 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
|
||||
// create chat with three members
|
||||
add_to_chat_contacts_table(
|
||||
&alice,
|
||||
time(),
|
||||
chat_id,
|
||||
&[
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
@@ -4244,45 +4165,37 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Bob removes Fiona.
|
||||
// bob removes a member
|
||||
let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?;
|
||||
let remove_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Bob adds new members "blue" and "orange", but first addition message is lost.
|
||||
// bob adds a new member
|
||||
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
|
||||
|
||||
let add_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice only receives the second member addition,
|
||||
// but this results in addition of both members
|
||||
// and removal of Fiona.
|
||||
// alice only receives the addition of the member
|
||||
alice.recv_msg(&add_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
|
||||
|
||||
// Alice re-adds Fiona.
|
||||
// since we missed a message, a new contact list should be build
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 3);
|
||||
|
||||
// readd fiona
|
||||
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
|
||||
|
||||
// Delayed removal of Fiona by Bob shouldn't remove her.
|
||||
alice.recv_msg(&remove_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
|
||||
|
||||
alice
|
||||
.golden_test_chat(chat_id, "receive_imf_delayed_removal_is_ignored")
|
||||
.await;
|
||||
// delayed removal of fiona shouldn't remove her
|
||||
alice.recv_msg_trash(&remove_msg).await;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
|
||||
@@ -4297,7 +4210,6 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Bob leaves, but Alice didn't receive Bob's leave message.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
@@ -4311,11 +4223,12 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
.await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Bob received a message from Alice, but this should not re-add him to the group.
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
// Bob got an update that fiora is added nevertheless.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
// 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?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4542,15 +4455,6 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
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?);
|
||||
|
||||
// Even if some time passed, Bob must not be re-added back.
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
send_text_msg(&alice, alice_chat_id, "5th message".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
send_text_msg(&alice, alice_chat_id, "6th 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(())
|
||||
}
|
||||
|
||||
@@ -4784,6 +4688,13 @@ async fn test_partial_group_consistency() -> Result<()> {
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
|
||||
// Get initial timestamp.
|
||||
let timestamp = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Bob receives partial message.
|
||||
let msg_id = receive_imf_from_inbox(
|
||||
&bob,
|
||||
@@ -4804,9 +4715,15 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp2 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Partial download does not change the member list.
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(timestamp, timestamp2);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
// Alice sends normal message to bob, adding fiona.
|
||||
@@ -4819,6 +4736,15 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
let timestamp3 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Receiving a message after a partial download recreates the member list because we treat
|
||||
// such messages as if we have not seen them.
|
||||
assert_ne!(timestamp, timestamp3);
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
|
||||
@@ -4842,9 +4768,15 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp4 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// After full download, the old message should not change group state.
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(timestamp3, timestamp4);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
Ok(())
|
||||
@@ -4867,13 +4799,19 @@ async fn test_leave_protected_group_missing_member_key() -> Result<()> {
|
||||
("b@b", "bob@example.net"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// We fail to send the message.
|
||||
assert!(remove_contact_from_chat(alice, group_id, ContactId::SELF)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// The contact is already removed anyway.
|
||||
assert!(is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE acpeerstates SET addr=? WHERE addr=?",
|
||||
("bob@example.net", "b@b"),
|
||||
)
|
||||
.await?;
|
||||
remove_contact_from_chat(alice, group_id, ContactId::SELF).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
assert!(!is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4895,22 +4833,12 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
let fiona = &tcm.fiona().await;
|
||||
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
|
||||
mark_as_verified(alice, fiona).await;
|
||||
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
|
||||
assert!(add_contact_to_chat(alice, group_id, alice_fiona_id)
|
||||
.await
|
||||
.is_err());
|
||||
// Sending the message failed,
|
||||
// but member is added to the chat locally already.
|
||||
assert!(is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
|
||||
let msg = alice.get_last_msg_in(group_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await
|
||||
);
|
||||
|
||||
assert!(!is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
|
||||
// Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that
|
||||
// may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a
|
||||
// normal scenario anyway.
|
||||
|
||||
@@ -14,31 +14,12 @@ use crate::{context::Context, log::LogExt};
|
||||
|
||||
use super::InnerSchedulerState;
|
||||
|
||||
/// Rough connectivity status for display in the status bar in the UI.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
|
||||
pub enum Connectivity {
|
||||
/// Not connected.
|
||||
///
|
||||
/// This may be because we just started,
|
||||
/// because we lost connection and
|
||||
/// were not able to connect and log in yet
|
||||
/// or because I/O is not started.
|
||||
NotConnected = 1000,
|
||||
|
||||
/// Attempting to connect and log in.
|
||||
Connecting = 2000,
|
||||
|
||||
/// Fetching or sending messages.
|
||||
/// Fetching or sending messages
|
||||
Working = 3000,
|
||||
|
||||
/// We are connected but not doing anything.
|
||||
///
|
||||
/// This is the most common state,
|
||||
/// so mobile UIs display the profile name
|
||||
/// instead of connectivity status in this state.
|
||||
/// Desktop UI displays "Connected" in the tooltip,
|
||||
/// which signals that no more messages
|
||||
/// are coming in.
|
||||
Connected = 4000,
|
||||
}
|
||||
|
||||
@@ -51,17 +32,13 @@ enum DetailedConnectivity {
|
||||
Error(String),
|
||||
#[default]
|
||||
Uninitialized,
|
||||
|
||||
/// Attempting to connect,
|
||||
/// until we successfully log in.
|
||||
Connecting,
|
||||
|
||||
/// Connection is just established,
|
||||
/// there may be work to do.
|
||||
Preparing,
|
||||
/// Connection is just established, but there may be work to do.
|
||||
Connected,
|
||||
|
||||
/// There is actual work to do, e.g. there are messages in SMTP queue
|
||||
/// or we detected a message on IMAP server that should be downloaded.
|
||||
/// or we detected a message that should be downloaded.
|
||||
Working,
|
||||
|
||||
InterruptingIdle,
|
||||
@@ -80,14 +57,8 @@ impl DetailedConnectivity {
|
||||
DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected),
|
||||
DetailedConnectivity::Connecting => Some(Connectivity::Connecting),
|
||||
DetailedConnectivity::Working => Some(Connectivity::Working),
|
||||
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working),
|
||||
|
||||
// At this point IMAP has just connected,
|
||||
// but does not know yet if there are messages to download.
|
||||
// We still convert this to Working state
|
||||
// so user can see "Updating..." and not "Connected"
|
||||
// which is reserved for idle state.
|
||||
DetailedConnectivity::Preparing => Some(Connectivity::Working),
|
||||
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Connected),
|
||||
DetailedConnectivity::Connected => Some(Connectivity::Connected),
|
||||
|
||||
// Just don't return a connectivity, probably the folder is configured not to be
|
||||
// watched or there is e.g. no "Sent" folder, so we are not interested in it
|
||||
@@ -103,9 +74,9 @@ impl DetailedConnectivity {
|
||||
| DetailedConnectivity::Uninitialized
|
||||
| DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(),
|
||||
DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
|
||||
DetailedConnectivity::Preparing
|
||||
| DetailedConnectivity::Working
|
||||
DetailedConnectivity::Working
|
||||
| DetailedConnectivity::InterruptingIdle
|
||||
| DetailedConnectivity::Connected
|
||||
| DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -115,12 +86,10 @@ impl DetailedConnectivity {
|
||||
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
|
||||
DetailedConnectivity::Uninitialized => "Not started".to_string(),
|
||||
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
|
||||
DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
|
||||
stock_str::updating(context).await
|
||||
}
|
||||
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
|
||||
stock_str::connected(context).await
|
||||
}
|
||||
DetailedConnectivity::Working => stock_str::updating(context).await,
|
||||
DetailedConnectivity::InterruptingIdle
|
||||
| DetailedConnectivity::Connected
|
||||
| DetailedConnectivity::Idle => stock_str::connected(context).await,
|
||||
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -138,7 +107,7 @@ impl DetailedConnectivity {
|
||||
// since sending the last message, connectivity could have changed, which we don't notice
|
||||
// until another message is sent
|
||||
DetailedConnectivity::InterruptingIdle
|
||||
| DetailedConnectivity::Preparing
|
||||
| DetailedConnectivity::Connected
|
||||
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await,
|
||||
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
|
||||
}
|
||||
@@ -151,7 +120,7 @@ impl DetailedConnectivity {
|
||||
DetailedConnectivity::Connecting => false,
|
||||
DetailedConnectivity::Working => false,
|
||||
DetailedConnectivity::InterruptingIdle => false,
|
||||
DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do.
|
||||
DetailedConnectivity::Connected => false, // Just connected, there may still be work to do.
|
||||
DetailedConnectivity::NotConfigured => true,
|
||||
DetailedConnectivity::Idle => true,
|
||||
}
|
||||
@@ -179,8 +148,8 @@ impl ConnectivityStore {
|
||||
pub(crate) async fn set_working(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::Working).await;
|
||||
}
|
||||
pub(crate) async fn set_preparing(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::Preparing).await;
|
||||
pub(crate) async fn set_connected(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::Connected).await;
|
||||
}
|
||||
pub(crate) async fn set_not_configured(&self, context: &Context) {
|
||||
self.set(context, DetailedConnectivity::NotConfigured).await;
|
||||
@@ -200,8 +169,8 @@ impl ConnectivityStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
|
||||
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
|
||||
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
|
||||
/// 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;
|
||||
@@ -210,7 +179,8 @@ pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Conne
|
||||
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
|
||||
// return Connected until DC is completely done with fetching folders; this also
|
||||
// includes scan_folders() which happens on the inbox thread.
|
||||
if *connectivity_lock == DetailedConnectivity::Idle
|
||||
if *connectivity_lock == DetailedConnectivity::Connected
|
||||
|| *connectivity_lock == DetailedConnectivity::Idle
|
||||
|| *connectivity_lock == DetailedConnectivity::NotConfigured
|
||||
{
|
||||
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
|
||||
@@ -219,7 +189,9 @@ pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<Conne
|
||||
|
||||
for state in oboxes {
|
||||
let mut connectivity_lock = state.0.lock().await;
|
||||
if *connectivity_lock == DetailedConnectivity::Idle {
|
||||
if *connectivity_lock == DetailedConnectivity::Connected
|
||||
|| *connectivity_lock == DetailedConnectivity::Idle
|
||||
{
|
||||
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +507,7 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns true if all background work is done.
|
||||
async fn all_work_done(&self) -> bool {
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
let lock = self.scheduler.inner.read().await;
|
||||
let stores: Vec<_> = match *lock {
|
||||
InnerSchedulerState::Started(ref sched) => sched
|
||||
@@ -555,23 +527,4 @@ impl Context {
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Waits until background work is finished.
|
||||
pub async fn wait_for_all_work_done(&self) {
|
||||
// Ideally we could wait for connectivity change events,
|
||||
// but sleep loop is good enough.
|
||||
|
||||
// First 100 ms sleep in chunks of 10 ms.
|
||||
for _ in 0..10 {
|
||||
if self.all_work_done().await {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
// If we are not finished in 100 ms, keep waking up every 100 ms.
|
||||
while !self.all_work_done().await {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,9 +454,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
} else {
|
||||
// Setup verified contact.
|
||||
secure_connection_established(
|
||||
@@ -471,8 +468,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
@@ -754,7 +751,7 @@ mod tests {
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::get_chat_msg;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
use std::collections::HashSet;
|
||||
@@ -801,8 +798,6 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
@@ -1356,8 +1351,6 @@ mod tests {
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
|
||||
@@ -59,13 +59,8 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// only become usable once the protocol is finished.
|
||||
let group_chat_id = state.joining_chat_id(context).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
group_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
.await?;
|
||||
chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
|
||||
.await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
|
||||
|
||||
21
src/smtp.rs
21
src/smtp.rs
@@ -21,6 +21,7 @@ use crate::mimefactory::MimeFactory;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::sql;
|
||||
use crate::stock_str::unencrypted_email;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
@@ -584,16 +585,18 @@ async fn send_mdn_rfc724_mid(
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,))
|
||||
.await?;
|
||||
if !additional_rfc724_mids.is_empty() {
|
||||
let q = format!(
|
||||
"DELETE FROM smtp_mdns WHERE rfc724_mid IN({})",
|
||||
sql::repeat_vars(additional_rfc724_mids.len())
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(&q, rusqlite::params_from_iter(additional_rfc724_mids))
|
||||
.await?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
SendResult::Retry => {
|
||||
|
||||
77
src/sql.rs
77
src/sql.rs
@@ -19,7 +19,6 @@ use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::http::http_cache_cleanup;
|
||||
use crate::net::prune_connection_history;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -44,6 +43,12 @@ macro_rules! params_slice {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn params_iter(
|
||||
iter: &[impl crate::sql::ToSql],
|
||||
) -> impl Iterator<Item = &dyn crate::sql::ToSql> {
|
||||
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
|
||||
}
|
||||
|
||||
mod migrations;
|
||||
mod pool;
|
||||
|
||||
@@ -435,7 +440,7 @@ impl Sql {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Execute the function inside a transaction assuming that it does writes.
|
||||
/// Execute the function inside a transaction assuming that it does write queries.
|
||||
///
|
||||
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
||||
/// error, the transaction will be committed.
|
||||
@@ -444,28 +449,7 @@ impl Sql {
|
||||
H: Send + 'static,
|
||||
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
{
|
||||
let query_only = false;
|
||||
self.transaction_ex(query_only, callback).await
|
||||
}
|
||||
|
||||
/// Execute the function inside a transaction.
|
||||
///
|
||||
/// * `query_only` - Whether the function only executes read statements (queries) and can be run
|
||||
/// in parallel with other transactions. NB: Creating and modifying temporary tables are also
|
||||
/// allowed with `query_only`, temporary tables aren't visible in other connections, but you
|
||||
/// need to pass `PRAGMA query_only=0;` to SQLite before that:
|
||||
/// `pragma_update(None, "query_only", "0")`.
|
||||
/// Also temporary tables need to be dropped because the connection is returned to the pool
|
||||
/// then.
|
||||
///
|
||||
/// If the function returns an error, the transaction will be rolled back. If it does not return
|
||||
/// an error, the transaction will be committed.
|
||||
pub async fn transaction_ex<G, H>(&self, query_only: bool, callback: G) -> Result<H>
|
||||
where
|
||||
H: Send + 'static,
|
||||
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
{
|
||||
self.call(query_only, move |conn| {
|
||||
self.call_write(move |conn| {
|
||||
let mut transaction = conn.transaction()?;
|
||||
let ret = callback(&mut transaction);
|
||||
|
||||
@@ -736,12 +720,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
warn!(context, "Can't set config: {e:#}.");
|
||||
}
|
||||
|
||||
http_cache_cleanup(context)
|
||||
.await
|
||||
.context("Failed to cleanup HTTP cache")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
if let Err(err) = remove_unused_files(context).await {
|
||||
warn!(
|
||||
context,
|
||||
@@ -868,22 +846,6 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
.await
|
||||
.context("housekeeping: failed to SELECT value FROM config")?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT blobname FROM http_cache",
|
||||
(),
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
for row in rows {
|
||||
maybe_add_file(&mut files_in_use, &row?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to SELECT blobname FROM http_cache")?;
|
||||
|
||||
info!(context, "{} files in use.", files_in_use.len());
|
||||
/* go through directories and delete unused files */
|
||||
let blobdir = context.get_blobdir();
|
||||
@@ -902,6 +864,7 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
|
||||
if p == blobdir
|
||||
&& (is_file_in_use(&files_in_use, None, &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s))
|
||||
{
|
||||
@@ -924,11 +887,13 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
}
|
||||
unreferenced_count += 1;
|
||||
let recently_created =
|
||||
stats.created().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_modified =
|
||||
stats.modified().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_accessed =
|
||||
stats.accessed().is_ok_and(|t| t > keep_files_newer_than);
|
||||
stats.created().map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_modified = stats
|
||||
.modified()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
let recently_accessed = stats
|
||||
.accessed()
|
||||
.map_or(false, |t| t > keep_files_newer_than);
|
||||
|
||||
if p == blobdir
|
||||
&& (recently_created || recently_modified || recently_accessed)
|
||||
@@ -1039,6 +1004,16 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to return comma-separated sequence of `?` chars.
|
||||
///
|
||||
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
|
||||
/// parameter lists.
|
||||
pub fn repeat_vars(count: usize) -> String {
|
||||
let mut s = "?,".repeat(count);
|
||||
s.pop(); // Remove trailing comma
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1070,107 +1070,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 124)?;
|
||||
if dbversion < migration_version {
|
||||
// Mark Saved Messages chat as protected if it already exists.
|
||||
sql.execute_migration(
|
||||
"UPDATE chats
|
||||
SET protected=1 -- ProtectionStatus::Protected
|
||||
WHERE type==100 -- Chattype::Single
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM chats_contacts cc
|
||||
WHERE cc.chat_id==chats.id
|
||||
AND cc.contact_id=1
|
||||
)
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 125)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE http_cache (
|
||||
url TEXT PRIMARY KEY,
|
||||
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
|
||||
blobname TEXT NOT NULL,
|
||||
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
|
||||
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
|
||||
) STRICT",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 126)?;
|
||||
if dbversion < migration_version {
|
||||
// Recreate http_cache table with new `stale` column.
|
||||
sql.execute_migration(
|
||||
"DROP TABLE http_cache;
|
||||
CREATE TABLE http_cache (
|
||||
url TEXT PRIMARY KEY,
|
||||
expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
|
||||
stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
|
||||
blobname TEXT NOT NULL,
|
||||
mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
|
||||
encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
|
||||
) STRICT",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 127)?;
|
||||
if dbversion < migration_version {
|
||||
// This is buggy: `delete_server_after` > 1 isn't handled. Migration #129 fixes this.
|
||||
sql.execute_migration(
|
||||
"INSERT OR IGNORE INTO config (keyname, value)
|
||||
SELECT 'bcc_self', '1'
|
||||
FROM config WHERE keyname='delete_server_after' AND value='0'
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 128)?;
|
||||
if dbversion < migration_version {
|
||||
// Add the timestamps of addition and removal.
|
||||
//
|
||||
// If `add_timestamp >= remove_timestamp`,
|
||||
// then the member is currently a member of the chat.
|
||||
// Otherwise the member is a past member.
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE chats_contacts
|
||||
ADD COLUMN add_timestamp NOT NULL DEFAULT 0;
|
||||
ALTER TABLE chats_contacts
|
||||
ADD COLUMN remove_timestamp NOT NULL DEFAULT 0;
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 129)?;
|
||||
if dbversion < migration_version {
|
||||
// Existing chatmail configurations having `delete_server_after` != "delete at once" should
|
||||
// get `bcc_self` enabled, they may be multidevice configurations:
|
||||
// - Before migration #127, `delete_server_after` was set to 0 upon a backup export, but
|
||||
// then `bcc_self` is enabled instead (whose default is changed to 0 for chatmail).
|
||||
// - The user might set `delete_server_after` to a value other than 0 or 1 when that was
|
||||
// possible in UIs.
|
||||
// We don't check `is_chatmail` for simplicity.
|
||||
sql.execute_migration(
|
||||
"INSERT OR IGNORE INTO config (keyname, value)
|
||||
SELECT 'bcc_self', '1'
|
||||
FROM config WHERE keyname='delete_server_after' AND value!='1'
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1231,35 +1130,6 @@ impl Sql {
|
||||
.await
|
||||
.with_context(|| format!("execute_migration failed for version {version}"))?;
|
||||
|
||||
self.config_cache.write().await.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_clear_config_cache() -> anyhow::Result<()> {
|
||||
// Some migrations change the `config` table in SQL.
|
||||
// This test checks that the config cache is invalidated in `execute_migration()`.
|
||||
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
|
||||
|
||||
t.sql
|
||||
.execute_migration(
|
||||
"INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
|
||||
1000,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
|
||||
assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
|
||||
|
||||
Ok(())
|
||||
self.set_db_version_in_cache(version).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl InnerPool {
|
||||
/// Retrieves a connection from the pool.
|
||||
///
|
||||
/// Sets `query_only` pragma to the provided value
|
||||
/// to prevent accidental misuse of connection
|
||||
/// to prevent accidentaly misuse of connection
|
||||
/// for writing when reading is intended.
|
||||
/// Only pass `query_only=false` if you want
|
||||
/// to use the connection for writing.
|
||||
|
||||
@@ -149,7 +149,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Message from %1$s"))]
|
||||
SubjectForNewContact = 73,
|
||||
|
||||
/// Unused. Was used in group chat status messages.
|
||||
#[strum(props(fallback = "Failed to send message to %1$s."))]
|
||||
FailedSendingTo = 74,
|
||||
|
||||
@@ -431,9 +430,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
|
||||
MsgReactedBy = 177,
|
||||
|
||||
#[strum(props(fallback = "Member %1$s removed."))]
|
||||
MsgDelMember = 178,
|
||||
|
||||
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
|
||||
SecurejoinWait = 190,
|
||||
|
||||
@@ -545,7 +541,7 @@ impl ContactId {
|
||||
.unwrap_or_else(|_| self.to_string())
|
||||
}
|
||||
|
||||
/// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
|
||||
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
|
||||
async fn get_stock_name(self, context: &Context) -> String {
|
||||
Contact::get_by_id(context, self)
|
||||
.await
|
||||
@@ -714,11 +710,7 @@ pub(crate) async fn msg_del_member_local(
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgDelMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouDelMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
@@ -988,6 +980,13 @@ pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str)
|
||||
.replace1(self_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Failed to send message to %1$s.`.
|
||||
pub(crate) async fn failed_sending_to(context: &Context, name: &str) -> String {
|
||||
translated(context, StockMessage::FailedSendingTo)
|
||||
.await
|
||||
.replace1(name)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is disabled.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_disabled(
|
||||
context: &Context,
|
||||
|
||||
@@ -566,10 +566,6 @@ mod tests {
|
||||
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await?);
|
||||
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
|
||||
|
||||
// Sync messages are "auto-generated", but they mustn't make the self-contact a bot.
|
||||
let self_contact = alice2.add_or_lookup_contact(&alice2).await;
|
||||
assert!(!self_contact.is_bot());
|
||||
|
||||
// the same sync message sent to bob must not be executed
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
|
||||
@@ -41,7 +41,6 @@ use crate::pgp::KeyPair;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::time;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
@@ -619,7 +618,7 @@ impl TestContext {
|
||||
.filter(|msg| msg.chat_id != DC_CHAT_ID_TRASH)
|
||||
}
|
||||
|
||||
/// Receives a message and asserts that it goes to trash chat.
|
||||
/// Recevies a message and asserts that it goes to trash chat.
|
||||
pub async fn recv_msg_trash(&self, msg: &SentMessage<'_>) {
|
||||
let received = receive_imf(self, msg.payload().as_bytes(), false)
|
||||
.await
|
||||
@@ -881,7 +880,7 @@ impl TestContext {
|
||||
let contact = self.add_or_lookup_contact(member).await;
|
||||
to_add.push(contact.id);
|
||||
}
|
||||
add_to_chat_contacts_table(self, time(), chat_id, &to_add)
|
||||
add_to_chat_contacts_table(self, chat_id, &to_add)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1352,24 +1351,6 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// When dropped after a test failure,
|
||||
/// prints a note about a possible false-possible caused by SystemTime::shift().
|
||||
pub(crate) struct TimeShiftFalsePositiveNote;
|
||||
impl Drop for TimeShiftFalsePositiveNote {
|
||||
fn drop(&mut self) {
|
||||
if std::thread::panicking() {
|
||||
let green = nu_ansi_term::Color::Green.normal();
|
||||
println!("{}", green.paint(
|
||||
"\nNOTE: This test failure may be a false-positive, caused by tests running in parallel.
|
||||
The issue is that `SystemTime::shift()` (a utility function for tests) changes the time for all threads doing tests, and not only for the running test.
|
||||
Until the false-positive is fixed:
|
||||
- Use `cargo test -- --test-threads 1` instead of `cargo test`
|
||||
- Or use `cargo nextest run` (install with `cargo install cargo-nextest --locked`)\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -371,7 +371,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
chat::add_contact_to_chat(&bob, group, bob_alice_contact).await?;
|
||||
|
||||
// Alice sends a message which Bob doesn't receive or something
|
||||
// A real attack would rather reuse a message that was sent to a group
|
||||
// A real attack would rather re-use a message that was sent to a group
|
||||
// and replace the Message-Id or so.
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(chat.id, "whoop whoop").await;
|
||||
|
||||
@@ -686,7 +686,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
alice.create_chat(&bob).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.len() == 1);
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
drop(bob);
|
||||
@@ -709,7 +709,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
assert_eq!(chat.is_protected(), false);
|
||||
assert_eq!(chat.is_protection_broken(), true);
|
||||
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.len() == 1);
|
||||
|
||||
{
|
||||
let alice_bob_chat = alice.get_chat(&bob_new).await;
|
||||
|
||||
@@ -74,8 +74,8 @@ pub struct WebxdcManifest {
|
||||
/// Optional URL of webxdc source code.
|
||||
pub source_code_url: Option<String>,
|
||||
|
||||
/// Set to "map" to request integration.
|
||||
pub request_integration: Option<String>,
|
||||
/// If the webxdc requests network access.
|
||||
pub request_internet_access: Option<bool>,
|
||||
}
|
||||
|
||||
/// Parsed information from WebxdcManifest and fallbacks.
|
||||
@@ -100,9 +100,6 @@ pub struct WebxdcInfo {
|
||||
/// URL of webxdc source code or an empty string.
|
||||
pub source_code_url: String,
|
||||
|
||||
/// Set to "map" to request integration, otherwise an empty string.
|
||||
pub request_integration: String,
|
||||
|
||||
/// If the webxdc is allowed to access the network.
|
||||
/// It should request access, be encrypted
|
||||
/// and sent to self for this.
|
||||
@@ -370,16 +367,18 @@ impl Context {
|
||||
.get_overwritable_info_msg_id(&instance, from_id)
|
||||
.await?;
|
||||
|
||||
if let (Some(info_msg_id), None) = (info_msg_id, &status_update_item.href) {
|
||||
chat::update_msg_text_and_timestamp(
|
||||
self,
|
||||
instance.chat_id,
|
||||
info_msg_id,
|
||||
info.as_str(),
|
||||
timestamp,
|
||||
)
|
||||
.await?;
|
||||
notify_msg_id = info_msg_id;
|
||||
if info_msg_id.is_some() && status_update_item.href.is_none() {
|
||||
if let Some(info_msg_id) = info_msg_id {
|
||||
chat::update_msg_text_and_timestamp(
|
||||
self,
|
||||
instance.chat_id,
|
||||
info_msg_id,
|
||||
info.as_str(),
|
||||
timestamp,
|
||||
)
|
||||
.await?;
|
||||
notify_msg_id = info_msg_id;
|
||||
}
|
||||
} else {
|
||||
notify_msg_id = chat::add_info_msg_with_cmd(
|
||||
self,
|
||||
@@ -417,11 +416,15 @@ impl Context {
|
||||
if from_id != ContactId::SELF {
|
||||
if let Some(notify_list) = status_update_item.notify {
|
||||
let self_addr = instance.get_webxdc_self_addr(self).await?;
|
||||
if let Some(notify_text) =
|
||||
notify_list.get(&self_addr).or_else(|| notify_list.get("*"))
|
||||
{
|
||||
if let Some(notify_text) = notify_list.get(&self_addr) {
|
||||
self.emit_event(EventType::IncomingWebxdcNotify {
|
||||
contact_id: from_id,
|
||||
msg_id: notify_msg_id,
|
||||
text: notify_text.clone(),
|
||||
href: status_update_item.href,
|
||||
});
|
||||
} else if let Some(notify_text) = notify_list.get("*") {
|
||||
self.emit_event(EventType::IncomingWebxdcNotify {
|
||||
chat_id: instance.chat_id,
|
||||
contact_id: from_id,
|
||||
msg_id: notify_msg_id,
|
||||
text: notify_text.clone(),
|
||||
@@ -919,9 +922,9 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
let request_integration = manifest.request_integration.unwrap_or_default();
|
||||
let is_integrated = self.is_set_as_webxdc_integration(context).await?;
|
||||
let internet_access = is_integrated;
|
||||
let internet_access = manifest.request_internet_access.unwrap_or_default()
|
||||
&& self.chat_id.is_self_talk(context).await.unwrap_or_default()
|
||||
&& self.get_showpadlock();
|
||||
|
||||
let self_addr = self.get_webxdc_self_addr(context).await?;
|
||||
|
||||
@@ -943,11 +946,8 @@ impl Message {
|
||||
.get(Param::WebxdcDocument)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
summary: if is_integrated {
|
||||
"🌍 Used as map. Delete to use default. Do not enter sensitive data".to_string()
|
||||
} else if request_integration == "map" {
|
||||
"🌏 To use as map, forward to \"Saved Messages\" again. Do not enter sensitive data"
|
||||
.to_string()
|
||||
summary: if internet_access {
|
||||
"Dev Mode: Do not enter sensitive data!".to_string()
|
||||
} else {
|
||||
self.param
|
||||
.get(Param::WebxdcSummary)
|
||||
@@ -959,7 +959,6 @@ impl Message {
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
request_integration,
|
||||
internet_access,
|
||||
self_addr,
|
||||
send_update_interval: context.ratelimit.read().await.update_interval(),
|
||||
@@ -2235,6 +2234,19 @@ sth_for_the = "future""#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webxdc_manifest_request_internet_access() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"request_internet_access = 3"#.as_bytes());
|
||||
assert!(result.is_err());
|
||||
let manifest = parse_webxdc_manifest(r#" source_code_url="https://foo.org""#.as_bytes())?;
|
||||
assert_eq!(manifest.request_internet_access, None);
|
||||
let manifest = parse_webxdc_manifest(r#" request_internet_access=false"#.as_bytes())?;
|
||||
assert_eq!(manifest.request_internet_access, Some(false));
|
||||
let manifest = parse_webxdc_manifest(r#"request_internet_access = true"#.as_bytes())?;
|
||||
assert_eq!(manifest.request_internet_access, Some(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_min_api_too_large() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -2645,16 +2657,16 @@ sth_for_the = "future""#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// check that `info.internet_access` is not set for normal, non-integrated webxdc -
|
||||
// even if they use the deprecated option `request_internet_access` in manifest.toml
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_no_internet_access() -> Result<()> {
|
||||
async fn test_webxdc_internet_access() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_id = t.get_self_chat().await.id;
|
||||
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
|
||||
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
||||
let broadcast_id = create_broadcast_list(&t).await?;
|
||||
|
||||
let mut first_test = true; // only the first test has all conditions for internet access
|
||||
|
||||
for e2ee in ["1", "0"] {
|
||||
t.set_config(Config::E2eeEnabled, Some(e2ee)).await?;
|
||||
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
||||
@@ -2677,7 +2689,11 @@ sth_for_the = "future""#
|
||||
.await?;
|
||||
let instance = Message::load_from_db(&t, instance_id).await?;
|
||||
let info = instance.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.internet_access, false);
|
||||
assert_eq!(info.internet_access, first_test);
|
||||
assert_eq!(info.summary.contains("Do not enter sensitive"), first_test);
|
||||
assert_eq!(info.summary.contains("real summary"), !first_test);
|
||||
|
||||
first_test = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,34 +57,14 @@ impl Context {
|
||||
}
|
||||
|
||||
// Check if a Webxdc shall be used as an integration and remember that.
|
||||
pub(crate) async fn update_webxdc_integration_database(
|
||||
&self,
|
||||
msg: &mut Message,
|
||||
context: &Context,
|
||||
) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let is_integration = if msg.param.get_int(Param::WebxdcIntegration).is_some() {
|
||||
true
|
||||
} else if msg.chat_id.is_self_talk(context).await? {
|
||||
let info = msg.get_webxdc_info(context).await?;
|
||||
if info.request_integration == "map" {
|
||||
msg.param.set_int(Param::WebxdcIntegration, 1);
|
||||
msg.update_param(context).await?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_integration {
|
||||
self.set_config_internal(
|
||||
Config::WebxdcIntegration,
|
||||
Some(&msg.id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
pub(crate) async fn update_webxdc_integration_database(&self, msg: &Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Webxdc && msg.param.get_int(Param::WebxdcIntegration).is_some()
|
||||
{
|
||||
self.set_config_internal(
|
||||
Config::WebxdcIntegration,
|
||||
Some(&msg.id.to_u32().to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -121,26 +101,11 @@ impl Message {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the message is an actually used as Webxdc integration.
|
||||
pub(crate) async fn is_set_as_webxdc_integration(&self, context: &Context) -> Result<bool> {
|
||||
if let Some(integration_id) = context
|
||||
.get_config_parsed::<u32>(Config::WebxdcIntegration)
|
||||
.await?
|
||||
{
|
||||
Ok(integration_id == self.id.to_u32())
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::TestContext;
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
@@ -161,65 +126,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_overwrite_default_integration() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = &t.get_self_chat().await;
|
||||
assert!(t.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
async fn assert_integration(t: &Context, name: &str) -> Result<()> {
|
||||
let integration_id = t.init_webxdc_integration(None).await?.unwrap();
|
||||
let integration = Message::load_from_db(t, integration_id).await?;
|
||||
let integration_info = integration.get_webxdc_info(t).await?;
|
||||
assert_eq!(integration_info.name, name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// set default integration
|
||||
let bytes = include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc");
|
||||
let file = t.get_blobdir().join("maps.xdc");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_webxdc_integration(file.to_str().unwrap()).await?;
|
||||
assert_integration(&t, "with some icon").await?;
|
||||
|
||||
// send a maps.xdc with insufficient manifest
|
||||
let mut msg = Message::new(Viewtype::Webxdc);
|
||||
msg.set_file_from_bytes(
|
||||
&t,
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
t.send_msg(self_chat.id, &mut msg).await;
|
||||
assert_integration(&t, "with some icon").await?; // still the default integration
|
||||
|
||||
// send a maps.xdc with manifest including the line `request_integration = "map"`
|
||||
let mut msg = Message::new(Viewtype::Webxdc);
|
||||
msg.set_file_from_bytes(
|
||||
&t,
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let sent = t.send_msg(self_chat.id, &mut msg).await;
|
||||
let info = msg.get_webxdc_info(&t).await?;
|
||||
assert!(info.summary.contains("Used as map"));
|
||||
assert_integration(&t, "Maps Test 2").await?;
|
||||
|
||||
// when maps.xdc is received on another device, the integration is not accepted (needs to be forwarded again)
|
||||
let t2 = TestContext::new_alice().await;
|
||||
let msg2 = t2.recv_msg(&sent).await;
|
||||
let info = msg2.get_webxdc_info(&t2).await?;
|
||||
assert!(info.summary.contains("To use as map,"));
|
||||
assert!(t2.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
// deleting maps.xdc removes the user's integration - the UI will go back to default calling set_webxdc_integration() then
|
||||
message::delete_msgs(&t, &[msg.id]).await?;
|
||||
assert!(t.init_webxdc_integration(None).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ mod tests {
|
||||
async fn test_maps_integration() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc");
|
||||
let bytes = include_bytes!("../../test-data/webxdc/mapstest.xdc");
|
||||
let file = t.get_blobdir().join("maps.xdc");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_webxdc_integration(file.to_str().unwrap()).await?;
|
||||
@@ -199,7 +199,7 @@ mod tests {
|
||||
|
||||
let integration = Message::load_from_db(&t, integration_id).await?;
|
||||
let info = integration.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.name, "Maps Test 2");
|
||||
assert_eq!(info.name, "Maps Test");
|
||||
assert_eq!(info.internet_access, true);
|
||||
|
||||
t.send_webxdc_status_update(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user