mirror of
https://github.com/chatmail/core.git
synced 2026-10-17 10:23:01 +03:00
Compare commits
101
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ab5c4f6b5 | ||
|
|
85d5b7b835 | ||
|
|
aff063e893 | ||
|
|
f302ce92de | ||
|
|
35168eec81 | ||
|
|
d2fa6c437c | ||
|
|
77cd5b6543 | ||
|
|
1c30daf4c2 | ||
|
|
5f7ca4ff9a | ||
|
|
dd73d23a0a | ||
|
|
3292ba260d | ||
|
|
5fe42f193e | ||
|
|
af42abd0aa | ||
|
|
c8803f6f05 | ||
|
|
3ad83ade12 | ||
|
|
d9ce231199 | ||
|
|
0a3787c389 | ||
|
|
8a278c3ee9 | ||
|
|
3129e20726 | ||
|
|
4ee65a049f | ||
|
|
bea7e4792c | ||
|
|
ded8c02c0f | ||
|
|
cbca5101b1 | ||
|
|
88278fc826 | ||
|
|
d8f07b2c5f | ||
|
|
4850e3696d | ||
|
|
d6c2c863b7 | ||
|
|
6abadac4bb | ||
|
|
55702e4985 | ||
|
|
9cb60f5f49 | ||
|
|
bb8b262e68 | ||
|
|
69fbb98f3c | ||
|
|
c98d3818d5 | ||
|
|
10aa308501 | ||
|
|
146bcfe455 | ||
|
|
f57cdc3a2c | ||
|
|
e11fddf9aa | ||
|
|
f396ff4297 | ||
|
|
51a1762228 | ||
|
|
69b4c0ccb4 | ||
|
|
3f1dfef0e7 | ||
|
|
c0f5771140 | ||
|
|
33cae2815d | ||
|
|
fc2b111f5d | ||
|
|
913d2c45b3 | ||
|
|
e32d676a08 | ||
|
|
9812d5ba75 | ||
|
|
bc7568e39b | ||
|
|
11bf1c45d2 | ||
|
|
122c23ad4e | ||
|
|
a0bde4699e | ||
|
|
ac01a4a771 | ||
|
|
51f2a8d59e | ||
|
|
f208c31cdf | ||
|
|
acd7a1d17e | ||
|
|
db6d451c90 | ||
|
|
4b3a6445fb | ||
|
|
aa3ef5011b | ||
|
|
1d3072c287 | ||
|
|
4fb59177fa | ||
|
|
d841bcb41e | ||
|
|
d205bc410b | ||
|
|
0d573ac037 | ||
|
|
a55e33fbc7 | ||
|
|
839b0e94af | ||
|
|
f2e600dc55 | ||
|
|
61fd0d400f | ||
|
|
7424d06416 | ||
|
|
aa71fbe04c | ||
|
|
c5cadd9991 | ||
|
|
c92554dc1f | ||
|
|
94c6d1dea4 | ||
|
|
d27d0ef476 | ||
|
|
d3f75360fa | ||
|
|
06a6cc48d2 | ||
|
|
b13f2709be | ||
|
|
1b824705fd | ||
|
|
6f22ce2722 | ||
|
|
5e58bf7575 | ||
|
|
85d7c1f942 | ||
|
|
df4fd82140 | ||
|
|
65b970a191 | ||
|
|
5e13b4c736 | ||
|
|
864833d232 | ||
|
|
3d07db6e62 | ||
|
|
9e88764a8a | ||
|
|
e70b879182 | ||
|
|
00d296e1ff | ||
|
|
e07f1a8b9c | ||
|
|
02b9085147 | ||
|
|
07fa9c35ee | ||
|
|
7db7c0aab1 | ||
|
|
30b23df816 | ||
|
|
4efd0d1ef7 | ||
|
|
f14880146a | ||
|
|
3a72188548 | ||
|
|
351f28361d | ||
|
|
c5b78741d6 | ||
|
|
57871bbaf8 | ||
|
|
287256693c | ||
|
|
d660f55a99 |
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.81.0
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -95,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.82.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
|
||||
- name: Run python tests
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
@@ -314,6 +314,6 @@ jobs:
|
||||
|
||||
- name: Run deltachat-rpc-client tests
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e py
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
- name: make sure websocket server version still builds
|
||||
working-directory: deltachat-jsonrpc
|
||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||
|
||||
@@ -64,5 +64,5 @@ jobs:
|
||||
working-directory: node
|
||||
run: npm run test
|
||||
env:
|
||||
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||
|
||||
+185
@@ -1,5 +1,183 @@
|
||||
# Changelog
|
||||
|
||||
## [1.148.6] - 2024-10-31
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add Message::new_text() ([#6123](https://github.com/deltachat/deltachat-core-rust/pull/6123)).
|
||||
- Add `MessageSearchResult.chat_id` ([#6120](https://github.com/deltachat/deltachat-core-rust/pull/6120)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Enable Webxdc realtime by default ([#6125](https://github.com/deltachat/deltachat-core-rust/pull/6125)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Save full text to mime_headers for long outgoing messages ([#6091](https://github.com/deltachat/deltachat-core-rust/pull/6091)).
|
||||
- Show root SMTP connection failure in connectivity view ([#6121](https://github.com/deltachat/deltachat-core-rust/pull/6121)).
|
||||
- Skip IDLE if we got unsolicited FETCH ([#6130](https://github.com/deltachat/deltachat-core-rust/pull/6130)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Silence another rust-analyzer false-positive ([#6124](https://github.com/deltachat/deltachat-core-rust/pull/6124)).
|
||||
- cargo: Upgrade iroh to 0.26.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Directly use connectives ([#6128](https://github.com/deltachat/deltachat-core-rust/pull/6128)).
|
||||
- Use Message::new_text() more ([#6127](https://github.com/deltachat/deltachat-core-rust/pull/6127)).
|
||||
|
||||
## [1.148.5] - 2024-10-27
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set Config::NotifyAboutWrongPw before saving configuration ([#5896](https://github.com/deltachat/deltachat-core-rust/pull/5896)).
|
||||
- Do not take write lock for maybe_network_lost() and set_push_device_token().
|
||||
- Do not lock the account manager for the whole duration of background_fetch.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Auto-restore 1:1 chat protection after receiving old unverified message.
|
||||
|
||||
### CI
|
||||
|
||||
- Take `CHATMAIL_DOMAIN` from variables instead of secrets.
|
||||
|
||||
### Other
|
||||
|
||||
- Revert "build: nix flake update fenix" to fix `nix build .#deltachat-rpc-server-armeabi-v7a-android`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Receive_imf::add_parts: Remove excessive `from_id == ContactId::SELF` checks.
|
||||
- Factor out `add_gossip_peer_from_header()`.
|
||||
|
||||
## [1.148.4] - 2024-10-24
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Jsonrpc: add `private_tag` to `Account::Configured` Object ([#6107](https://github.com/deltachat/deltachat-core-rust/pull/6107)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Normalize proxy URLs before saving into proxy_url.
|
||||
- Do not wait for connections in maybe_add_gossip_peers().
|
||||
|
||||
## [1.148.3] - 2024-10-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix reception of realtime advertisements.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Allow sending realtime messages up to 128 KB in size.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix DC_QR_PROXY docs ([#6099](https://github.com/deltachat/deltachat-core-rust/pull/6099)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Generate topic inside create_iroh_header().
|
||||
|
||||
### Tests
|
||||
|
||||
- Test that realtime advertisements work after chatting.
|
||||
|
||||
## [1.148.2] - 2024-10-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never initialize Iroh if realtime is disabled.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add more logging for iroh initialization and peer addition.
|
||||
|
||||
### Build system
|
||||
|
||||
- `nix flake update nixpkgs`.
|
||||
- `nix flake update fenix`.
|
||||
|
||||
## [1.148.1] - 2024-10-23
|
||||
|
||||
### Build system
|
||||
|
||||
- Revert "build: nix flake update"
|
||||
|
||||
This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
|
||||
|
||||
## [1.148.0] - 2024-10-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Create QR codes from any data ([#6090](https://github.com/deltachat/deltachat-core-rust/pull/6090)).
|
||||
- Add delta chat logo to QR codes ([#6093](https://github.com/deltachat/deltachat-core-rust/pull/6093)).
|
||||
- Add realtime advertisement received event ([#6043](https://github.com/deltachat/deltachat-core-rust/pull/6043)).
|
||||
- Notify adding reactions ([#6072](https://github.com/deltachat/deltachat-core-rust/pull/6072))
|
||||
- Internal profile names ([#6088](https://github.com/deltachat/deltachat-core-rust/pull/6088)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- IMAP COMPRESS support.
|
||||
- Sort received outgoing message down if it's fresher than all non fresh messages.
|
||||
- Prioritize cached results if DNS resolver returns many results.
|
||||
- Add in-memory cache for DNS.
|
||||
- deltachat-repl: Built-in QR code printer.
|
||||
- Log the logic for (not) doing AEAP.
|
||||
- Log when late Autocrypt header is ignored.
|
||||
- Add more context to `send_msg` errors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Replace old draft with a new one atomically.
|
||||
- ChatId::maybe_delete_draft: Don't delete message if it's not a draft anymore ([#6053](https://github.com/deltachat/deltachat-core-rust/pull/6053)).
|
||||
- Call update_connection_history for proxified connections.
|
||||
- sql: Set PRAGMA query_only to avoid writing on read-only connections.
|
||||
- sql: Run `PRAGMA incremental_vacuum` on a write connection.
|
||||
- Increase MAX_SECONDS_TO_LEND_FROM_FUTURE to 30.
|
||||
|
||||
### Build system
|
||||
|
||||
- Nix flake update.
|
||||
- Resolve warning about default-features, and make it possible to disable vendoring ([#6079](https://github.com/deltachat/deltachat-core-rust/pull/6079)).
|
||||
- Silence a rust-analyzer false-positive ([#6077](https://github.com/deltachat/deltachat-core-rust/pull/6077)).
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.82.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Set_protection_for_timestamp_sort does not send messages.
|
||||
- Document MimeFactory.req_mdn.
|
||||
- Fix `too_long_first_doc_paragraph` clippy lint.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Update_msg_state: Don't avoid downgrading OutMdnRcvd to OutDelivered.
|
||||
- Fix elided_named_lifetimes warning.
|
||||
- set_protection_for_timestamp_sort: Do not log bubbled up errors.
|
||||
- Fix clippy::needless_lifetimes warnings.
|
||||
- Use `HeaderDef` constant for Chat-Disposition-Notification-To.
|
||||
- Resultify get_self_fingerprint().
|
||||
- sql: Move write mutex into connection pool.
|
||||
|
||||
### Tests
|
||||
|
||||
- test_qr_setup_contact_svg: Stop testing for no display name.
|
||||
- Always gossip if gossip_period is set to 0.
|
||||
- test_aeap_flow_verified: Wait for "member added" before sending messages ([#6057](https://github.com/deltachat/deltachat-core-rust/pull/6057)).
|
||||
- Make test_verified_group_member_added_recovery more reliable.
|
||||
- test_aeap_flow_verified: Do not start ac1new.
|
||||
- Fix `test_securejoin_after_contact_resetup` flakiness.
|
||||
- Message from old setup preserves contact verification, but breaks 1:1 protection.
|
||||
|
||||
## [1.147.1] - 2024-10-13
|
||||
|
||||
### Build system
|
||||
@@ -5008,3 +5186,10 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.146.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.145.0..v1.146.0
|
||||
[1.147.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.146.0..v1.147.0
|
||||
[1.147.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.0..v1.147.1
|
||||
[1.148.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.147.1..v1.148.0
|
||||
[1.148.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.0..v1.148.1
|
||||
[1.148.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.1..v1.148.2
|
||||
[1.148.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.2..v1.148.3
|
||||
[1.148.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.3..v1.148.4
|
||||
[1.148.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.4..v1.148.5
|
||||
[1.148.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.148.5..v1.148.6
|
||||
|
||||
+1
-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 --no-default-features --features jsonrpc
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
|
||||
Generated
+188
-90
@@ -221,7 +221,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -233,7 +233,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -338,14 +338,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928ea96f04e1260036cb01f82c1e5dc7c3b478e7b9463c87712297784ce3cdc6"
|
||||
checksum = "00d1f1a16e5abad3ada9f1f23dbc2f354b138121b90533381be62dada6cbf40a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.13.1",
|
||||
@@ -366,7 +366,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -377,7 +377,7 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"futures-lite 2.3.0",
|
||||
"futures-lite 2.4.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -685,9 +685,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -991,6 +991,16 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
|
||||
|
||||
[[package]]
|
||||
name = "cordyceps"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3"
|
||||
dependencies = [
|
||||
"loom",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1109,6 +1119,28 @@ version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"crossterm_winapi",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
@@ -1203,7 +1235,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1227,7 +1259,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1238,7 +1270,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1271,7 +1303,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1295,7 +1327,7 @@ dependencies = [
|
||||
"fd-lock",
|
||||
"format-flowed",
|
||||
"futures",
|
||||
"futures-lite 2.3.0",
|
||||
"futures-lite 2.4.0",
|
||||
"hex",
|
||||
"hickory-resolver",
|
||||
"http-body-util",
|
||||
@@ -1371,7 +1403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1396,13 +1428,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"dirs",
|
||||
"log",
|
||||
"nu-ansi-term",
|
||||
"qr2term",
|
||||
"rusqlite",
|
||||
"rustyline",
|
||||
"tokio",
|
||||
@@ -1411,12 +1444,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"deltachat-jsonrpc",
|
||||
"futures-lite 2.3.0",
|
||||
"futures-lite 2.4.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1435,12 +1468,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1490,7 +1523,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1521,7 +1554,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1531,27 +1564,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0-beta.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3249c0372e72f5f93b5c0ca54c0ab76bbf6216b6f718925476fd9bc4ffabb4fe"
|
||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0-beta.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27d919ced7590fc17b5d5a3c63b662e8a7d2324212c4e4dbbed975cafd22d16d"
|
||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1564,6 +1597,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diatomic-waker"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -1611,7 +1650,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1882,7 +1921,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1902,7 +1941,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2189,12 +2228,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-buffered"
|
||||
version = "0.2.6"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02dcae03ee5afa5ea17b1aebc793806b8ddfc6dc500e0b8e8e1eb30b9dad22c0"
|
||||
checksum = "34acda8ae8b63fbe0b2195c998b180cff89a8212fb2622a78b572a9f1c6f7684"
|
||||
dependencies = [
|
||||
"cordyceps",
|
||||
"diatomic-waker",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -2263,9 +2303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
|
||||
dependencies = [
|
||||
"fastrand 2.1.1",
|
||||
"futures-core",
|
||||
@@ -2282,7 +2322,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2321,6 +2361,19 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2752,9 +2805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -2920,9 +2973,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "iroh-base"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545424889bce87e44fd08dac9e6889635630fdbdaaa858a3c5a5f0adbb8d3779"
|
||||
checksum = "973c0b3c7851fa2e8e6cf4cb81c9f4cab1373848828fafa43dfe25b123a89ff2"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"anyhow",
|
||||
@@ -2961,9 +3014,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-gossip"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a5489a563e407fb2be654e950536da4230e46642111f12b16d7013228164aae"
|
||||
checksum = "b44749f361aca7cddcb357b62ef84cad1023771aee03d427c8922991fc4a9561"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -2971,7 +3024,7 @@ dependencies = [
|
||||
"derive_more",
|
||||
"ed25519-dalek",
|
||||
"futures-concurrency",
|
||||
"futures-lite 2.3.0",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-util",
|
||||
"indexmap",
|
||||
"iroh-base",
|
||||
@@ -2989,9 +3042,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-metrics"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb6ab80f7cccde80be07a643308e1ff925d5be91721ec65ba96b261a0bcb5e5"
|
||||
checksum = "02edfa7ca1aa89b0b9793d94671e32b79c97d61e9022f082c5eb2b95b64e90c0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"erased_set",
|
||||
@@ -3010,9 +3063,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-net"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a099a60478cb9319153756a9cf2ff41aa03d18403690a91e60f049ad467b057"
|
||||
checksum = "329553be056a92eec7802e1c087eed951eec6e60f0f0740491ef16a295be4ca2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backoff",
|
||||
@@ -3023,7 +3076,7 @@ dependencies = [
|
||||
"duct",
|
||||
"futures-buffered",
|
||||
"futures-concurrency",
|
||||
"futures-lite 2.3.0",
|
||||
"futures-lite 2.4.0",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"governor",
|
||||
@@ -3069,6 +3122,7 @@ dependencies = [
|
||||
"time 0.3.36",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite-wasm",
|
||||
"tokio-util",
|
||||
@@ -3327,6 +3381,19 @@ version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
@@ -3677,7 +3744,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3738,7 +3805,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3812,7 +3879,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4016,7 +4083,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4096,22 +4163,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
|
||||
checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.5"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4223,7 +4290,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4406,7 +4473,7 @@ dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4438,7 +4505,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4457,6 +4524,22 @@ dependencies = [
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qr2term"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6867c60b38e9747a079a19614dbb5981a53f21b9a56c265f3bfdf6011a50a957"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen"
|
||||
version = "1.8.0"
|
||||
@@ -4486,9 +4569,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
|
||||
checksum = "ffbfb3ddf5364c9cfcd65549a1e7b801d0e8d1b14c1a1590a6408aa93cfbfa84"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5000,9 +5083,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.13"
|
||||
version = "0.23.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
|
||||
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -5172,9 +5255,15 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -5270,7 +5359,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5281,7 +5370,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5642,7 +5731,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"struct_iterable_internal",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5670,7 +5759,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5732,9 +5821,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
version = "2.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5761,7 +5850,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5851,22 +5940,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.64"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.64"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5983,7 +6072,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6168,7 +6257,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6280,7 +6369,7 @@ dependencies = [
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6406,9 +6495,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom 0.2.12",
|
||||
"serde",
|
||||
@@ -6502,7 +6591,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -6536,7 +6625,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -6632,6 +6721,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.51.1"
|
||||
@@ -6680,7 +6778,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6691,7 +6789,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6997,7 +7095,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7023,7 +7121,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7043,7 +7141,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn 2.0.86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -61,10 +61,10 @@ hickory-resolver = "=0.25.0-alpha.2"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.9"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.25.0", default-features = false }
|
||||
iroh-gossip = { version = "0.26.0", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.26.0", default-features = false }
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
@@ -79,14 +79,14 @@ percent-encoding = "2.3"
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.36"
|
||||
quick-xml = "0.37"
|
||||
quoted_printable = "0.5"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.9.0"
|
||||
rustls = { version = "0.23.13", default-features = false }
|
||||
rustls = { version = "0.23.14", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
@@ -169,10 +169,10 @@ async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.38", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc" }
|
||||
deltachat = { path = "." }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.30"
|
||||
futures-lite = "2.3.0"
|
||||
futures-lite = "2.4.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
transform="scale(1.1342891,0.88160947)">
|
||||
<path
|
||||
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
|
||||
</g>
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -522,14 +522,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
* fresh message and badge counters are not changed by this setting,
|
||||
* but should be tuned down where appropriate.
|
||||
* - `private_tag` = Optional tag as "Work", "Family".
|
||||
* Meant to help profile owner to differ between profiles with similar names.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
|
||||
* however, are not handled by the core otherwise.
|
||||
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
|
||||
* 1 = WebXDC realtime API is enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop.
|
||||
* 1 = WebXDC realtime API is enabled (default).
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -2531,8 +2533,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port:
|
||||
* ask the user if they want to use the given proxy and overwrite the previous one, if any.
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
@@ -2609,6 +2611,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
|
||||
* See dc_get_securejoin_qr() for details about the contained QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
|
||||
@@ -2789,6 +2792,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
* Create a QR code from any input data.
|
||||
*
|
||||
* The QR code is returned as a square SVG image.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param payload The content for the QR code.
|
||||
* @return SVG image with the QR code.
|
||||
* On errors, an empty string is returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_create_qr_svg (const char* payload);
|
||||
|
||||
|
||||
/**
|
||||
* Get last error string.
|
||||
*
|
||||
@@ -2877,6 +2896,7 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
|
||||
* This works like dc_backup_provider_qr() but returns the text of a rendered
|
||||
* SVG image containing the QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
@@ -2916,7 +2936,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
|
||||
* Gets a backup offered by a dc_backup_provider_t object on another device.
|
||||
*
|
||||
* This function is called on a device that scanned the QR code offered by
|
||||
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
|
||||
* dc_backup_provider_get_qr(). Typically this is a
|
||||
* different device than that which provides the backup.
|
||||
*
|
||||
* This call will block while the backup is being transferred and only
|
||||
@@ -6031,6 +6051,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_REACTIONS_CHANGED 2001
|
||||
|
||||
|
||||
/**
|
||||
* A reaction to one's own sent message received.
|
||||
* Typically, the UI will show a notification for that.
|
||||
*
|
||||
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
|
||||
*
|
||||
* @param data1 (int) contact_id ID of the contact sending this reaction.
|
||||
* @param data2 (int) msg_id + (char*) reaction.
|
||||
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
|
||||
* and the reaction as dc_event_get_data2_str().
|
||||
* string must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_REACTION 2002
|
||||
|
||||
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
@@ -6248,7 +6283,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* Webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||
* `serial` set to the last known update
|
||||
* (in case of special bots, `status_update_serial` from `data2`
|
||||
@@ -6283,6 +6318,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
|
||||
|
||||
/**
|
||||
* Advertisement for ephemeral peer channel communication received.
|
||||
* This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 0
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
|
||||
@@ -30,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
@@ -541,6 +541,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::ReactionsChanged { .. } => 2001,
|
||||
EventType::IncomingReaction { .. } => 2002,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
@@ -563,10 +564,14 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::WebxdcRealtimeData { .. } => 2150,
|
||||
EventType::WebxdcRealtimeAdvertisementReceived { .. } => 2151,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -621,11 +627,15 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
EventType::EventChannelOverflow { n } => *n as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,9 +676,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
@@ -682,6 +694,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +748,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -754,6 +770,14 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
EventType::IncomingReaction { reaction, .. } => reaction
|
||||
.as_str()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2570,6 +2594,18 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
eprintln!("ignoring careless call to dc_create_qr_svg()");
|
||||
return "".strdup();
|
||||
}
|
||||
|
||||
create_qr_svg(&to_string_lossy(payload))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
@@ -4832,7 +4868,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
block_on(async move { accounts.read().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4846,12 +4882,12 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move {
|
||||
let accounts = accounts.read().await;
|
||||
accounts
|
||||
.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||
.await;
|
||||
});
|
||||
let background_fetch_future = {
|
||||
let lock = block_on(accounts.read());
|
||||
lock.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||
};
|
||||
// At this point account manager is not locked anymore.
|
||||
block_on(background_fetch_future);
|
||||
1
|
||||
}
|
||||
|
||||
@@ -4869,7 +4905,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||
let token = to_string_lossy(token);
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
let accounts = accounts.read().await;
|
||||
if let Err(err) = accounts.set_push_device_token(&token).await {
|
||||
accounts.emit_event(EventType::Error(format!(
|
||||
"Failed to set notify token: {err:#}."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
+35
-10
@@ -4,8 +4,8 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
|
||||
|
||||
The JSON-RPC API is exposed in two fashions:
|
||||
|
||||
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||
- A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
||||
- The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||
|
||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
|
||||
|
||||
@@ -24,16 +24,17 @@ If you want to use the server in a production setup, first build it in release m
|
||||
```sh
|
||||
cargo build --features webserver --release
|
||||
```
|
||||
|
||||
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
|
||||
|
||||
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
|
||||
|
||||
The server can be configured with environment variables:
|
||||
|
||||
|variable|default|description|
|
||||
|-|-|-|
|
||||
|`DC_PORT`|`20808`|port to listen on|
|
||||
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
|
||||
| variable | default | description |
|
||||
| ------------------ | ------------ | ------------------------- |
|
||||
| `DC_PORT` | `20808` | port to listen on |
|
||||
| `DC_ACCOUNTS_PATH` | `./accounts` | path to storage directory |
|
||||
|
||||
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
|
||||
|
||||
@@ -43,30 +44,53 @@ cross build --features=webserver --target armv7-linux-androideabi --release
|
||||
|
||||
#### Using the TypeScript/JavaScript client
|
||||
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
||||
|
||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
|
||||
The package is also published on npm under the name [`@deltachat/jsonrpc-client`](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
|
||||
|
||||
###### Usage
|
||||
|
||||
Stdio server (recommended):
|
||||
|
||||
```typescript
|
||||
import { DeltaChat } from './deltachat.bundle.js'
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
const accounts = await dc.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
dc.close();
|
||||
```
|
||||
|
||||
Websocket:
|
||||
|
||||
```typescript
|
||||
import { WebsocketDeltaChat as DeltaChat } from '@deltachat/jsonrpc-client''=
|
||||
|
||||
const dc = new DeltaChat('ws://localhost:20808/ws')
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
const accounts = await dc.rpc.getAllAccounts()
|
||||
console.log('accounts', accounts)
|
||||
```
|
||||
|
||||
##### Generate TypeScript/JavaScript documentation
|
||||
|
||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm run docs
|
||||
```
|
||||
|
||||
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
||||
|
||||
## Development
|
||||
@@ -81,6 +105,7 @@ npm run build
|
||||
npm run example:build
|
||||
npm run example:start
|
||||
```
|
||||
|
||||
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
|
||||
|
||||
Run `npm run example:dev` to live-rebuild the example app when files changes.
|
||||
@@ -104,7 +129,7 @@ cd typescript
|
||||
npm run test
|
||||
```
|
||||
|
||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
|
||||
This will build the `deltachat-rpc-server` binary and then run a test suite against the deltachat-rpc-server (stdio).
|
||||
|
||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||
|
||||
|
||||
@@ -254,11 +254,12 @@ impl CommandApi {
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
self.accounts
|
||||
.write()
|
||||
.await
|
||||
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
.await;
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||
};
|
||||
// At this point account manager is not locked anymore.
|
||||
future.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1946,9 +1947,13 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = data.create_message(&ctx).await?;
|
||||
let mut message = data
|
||||
.create_message(&ctx)
|
||||
.await
|
||||
.context("Failed to create message")?;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.await
|
||||
.context("Failed to send created message")?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
}
|
||||
@@ -2113,8 +2118,7 @@ impl CommandApi {
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(text);
|
||||
let mut msg = Message::new_text(text);
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
|
||||
@@ -17,6 +17,9 @@ pub enum Account {
|
||||
// size: u32,
|
||||
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||
color: String,
|
||||
/// Optional tag as "Work", "Family".
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
private_tag: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Unconfigured { id: u32 },
|
||||
@@ -31,12 +34,14 @@ impl Account {
|
||||
let color = color_int_to_hex_string(
|
||||
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
|
||||
);
|
||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
||||
Ok(Account::Configured {
|
||||
id,
|
||||
display_name,
|
||||
addr,
|
||||
profile_image,
|
||||
color,
|
||||
private_tag,
|
||||
})
|
||||
} else {
|
||||
Ok(Account::Unconfigured { id })
|
||||
|
||||
@@ -98,6 +98,14 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
},
|
||||
|
||||
/// Incoming reaction, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingReaction {
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
reaction: String,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -244,6 +252,11 @@ pub enum EventType {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
|
||||
|
||||
/// Advertisement received over an ephemeral peer channel.
|
||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
@@ -297,6 +310,15 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
} => IncomingReaction {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -373,6 +395,11 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
data,
|
||||
},
|
||||
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
|
||||
WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: msg_id.to_u32(),
|
||||
}
|
||||
}
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
@@ -382,6 +409,9 @@ impl From<CoreEventType> for EventType {
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,7 @@ pub struct MessageSearchResult {
|
||||
author_name: String,
|
||||
author_color: String,
|
||||
author_id: u32,
|
||||
chat_id: u32,
|
||||
chat_profile_image: Option<String>,
|
||||
chat_color: String,
|
||||
chat_name: String,
|
||||
@@ -529,6 +530,7 @@ impl MessageSearchResult {
|
||||
author_name,
|
||||
author_color: color_int_to_hex_string(sender.get_color()),
|
||||
author_id: sender.id.to_u32(),
|
||||
chat_id: chat.id.to_u32(),
|
||||
chat_name: chat.get_name().to_owned(),
|
||||
chat_color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
@@ -605,16 +607,13 @@ impl MessageData {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = self.quoted_message_id {
|
||||
let quoted_message = Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("Failed to load quoted message")?;
|
||||
message
|
||||
.set_quote(
|
||||
context,
|
||||
Some(
|
||||
&Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
.set_quote(context, Some("ed_message))
|
||||
.await
|
||||
.context("Failed to set quote")?;
|
||||
} else if let Some(text) = self.quoted_text {
|
||||
let protect = false;
|
||||
message.set_quote_text(Some((text, protect)));
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# @deltachat/jsonrpc-client
|
||||
|
||||
This package is a client for the jsonrpc server.
|
||||
|
||||
> If you are looking for the functions in the documentation, they are under [`RawClient`](https://js.jsonrpc.delta.chat/classes/RawClient.html).
|
||||
|
||||
### Important Terms
|
||||
|
||||
- [delta chat core](https://github.com/deltachat/deltachat-core-rust/) the heart of all Delta Chat clients. Handels all the heavy lifting (email, encryption, ...) and provides an easy api for bots and clients (`getChatlist`, `getChat`, `getContact`, ...).
|
||||
- [jsonrpc](https://www.jsonrpc.org/specification) is a json based protocol
|
||||
for applications to speak to each other by [remote procedure calls](https://en.wikipedia.org/wiki/Remote_procedure_call) (short RPC),
|
||||
which basically means that the client can call methods on the server by sending a json messages.
|
||||
- [`deltachat-rpc-server`](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server) provides the jsonrpc api over stdio (stdin/stdout)
|
||||
- [`@deltachat/stdio-rpc-server`](https://www.npmjs.com/package/@deltachat/stdio-rpc-server) is an easy way to install `deltachat-rpc-server` from npm and use it from nodejs.
|
||||
|
||||
#### Transport
|
||||
|
||||
You need to connect this client to an instance of deltachat core via a transport.
|
||||
|
||||
Currently there are 2 transports available:
|
||||
|
||||
- (recomended) `StdioTransport` usable from `StdioDeltaChat` - speak to `deltachat-rpc-server` directly
|
||||
- `WebsocketTransport` usable from `WebsocketDeltaChat`
|
||||
|
||||
You can also make your own transport, for example deltachat desktop uses a custom transport that sends the json messages over electron ipc.
|
||||
Just implement your transport based on the `Transport` interface - look at how the [stdio transport is implemented](https://github.com/deltachat/deltachat-core-rust/blob/7121675d226e69fd85d0194d4b9c4442e4dd8299/deltachat-jsonrpc/typescript/src/client.ts#L113) for an example, it's not hard.
|
||||
|
||||
## Usage
|
||||
|
||||
> The **minimum** nodejs version for `@deltachat/stdio-rpc-server` is `16`
|
||||
|
||||
```
|
||||
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||
```
|
||||
|
||||
```js
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
// Import constants you might need later
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close();
|
||||
}
|
||||
main();
|
||||
```
|
||||
|
||||
For a more complete example refer to <https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc>.
|
||||
|
||||
### Listening for events
|
||||
|
||||
```ts
|
||||
dc.on("Info", (accountId, { msg }) =>
|
||||
console.info(accountId, "[core:info]", msg)
|
||||
);
|
||||
// Or get an event emitter for only one account
|
||||
const emitter = dc.getContextEvents(accountId);
|
||||
emitter.on("IncomingMsg", async ({ chatId, msgId }) => {
|
||||
const message = await dc.rpc.getMessage(accountId, msgId);
|
||||
console.log("got message in chat " + chatId + " : ", message.text);
|
||||
});
|
||||
```
|
||||
|
||||
### Getting Started
|
||||
|
||||
This section describes how to handle the Delta Chat core library over the jsonrpc bindings.
|
||||
For general information about Delta Chat itself,
|
||||
see <https://delta.chat> and <https://github.com/deltachat>.
|
||||
|
||||
Let's start.
|
||||
|
||||
First of all, you have to start the deltachat-rpc-server process.
|
||||
|
||||
```js
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
```
|
||||
|
||||
Then we have to create an Account (also called Context or profile) that is bound to a database.
|
||||
The database is a normal SQLite file with a "blob directory" beside it.
|
||||
But these details are handled by deltachat's account manager.
|
||||
So you just have to tell the account manager to create a new account:
|
||||
|
||||
```js
|
||||
const accountId = await dc.rpc.addAccount();
|
||||
```
|
||||
|
||||
After that, register event listeners so you can see what core is doing:
|
||||
Intenally `@deltachat/jsonrpc-client` implments a loop that waits for new events and then emits them to javascript land.
|
||||
```js
|
||||
dc.on("Info", (accountId, { msg }) =>
|
||||
console.info(accountId, "[core:info]", msg)
|
||||
);
|
||||
```
|
||||
|
||||
Now you can **configure the account:**
|
||||
```js
|
||||
// use some real test credentials here
|
||||
await dc.rpc.setConfig(accountId, "addr", "alice@example.org")
|
||||
await dc.rpc.setConfig(accountId, "mail_pw", "***")
|
||||
// you can also set multiple config options in one call
|
||||
await dc.rpc.batchSetConfig(accountId, {
|
||||
"addr": "alice@example.org",
|
||||
"mail_pw": "***"
|
||||
})
|
||||
|
||||
// after setting the credentials attempt to login
|
||||
await dc.rpc.configure(accountId)
|
||||
```
|
||||
|
||||
`configure()` returns a promise that is rejected on error (with await is is thrown).
|
||||
The configuration itself may take a while. You can monitor it's progress like this:
|
||||
```js
|
||||
dc.on("ConfigureProgress", (accountId, { progress, comment }) => {
|
||||
console.log(accountId, "ConfigureProgress", progress, comment);
|
||||
});
|
||||
// make sure to register this event handler before calling `dc.rpc.configure()`
|
||||
```
|
||||
|
||||
The configuration result is saved in the database.
|
||||
On subsequent starts it is not needed to call `dc.rpc.configure(accountId)`
|
||||
(you can check this using `dc.rpc.isConfigured(accountId)`).
|
||||
|
||||
On a successfully configuration delta chat core automatically connects to the server, however subsequent starts you **need to do that manually** by calling `dc.rpc.startIo(accountId)` or `dc.rpc.startIoForAllAccounts()`.
|
||||
|
||||
```js
|
||||
if (!await dc.rpc.isConfigured(accountId)) {
|
||||
// use some real test credentials here
|
||||
await dc.rpc.batchSetConfig(accountId, {
|
||||
"addr": "alice@example.org",
|
||||
"mail_pw": "***"
|
||||
})
|
||||
await dc.rpc.configure(accountId)
|
||||
} else {
|
||||
await dc.rpc.startIo(accountId)
|
||||
}
|
||||
```
|
||||
|
||||
Now you can **send the first message:**
|
||||
|
||||
```js
|
||||
const contactId = await dc.rpc.createContact(accountId, "bob@example.org", null /* optional name */)
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId, contactId)
|
||||
|
||||
await dc.rpc.miscSendTextMessage(accountId, chatId, "Hi, here is my first message!")
|
||||
```
|
||||
|
||||
`dc.rpc.miscSendTextMessage()` returns immediately;
|
||||
the sending itself is done in the background.
|
||||
If you check the testing address (bob),
|
||||
you should receive a normal e-mail.
|
||||
Answer this e-mail in any e-mail program with "Got it!",
|
||||
and the IO you started above will **receive the message**.
|
||||
|
||||
You can then **list all messages** of a chat as follows:
|
||||
|
||||
```js
|
||||
let i = 0;
|
||||
for (const msgId of await exp.rpc.getMessageIds(120, 12, false, false)) {
|
||||
i++;
|
||||
console.log(`Message: ${i}`, (await dc.rpc.getMessage(120, msgId)).text);
|
||||
}
|
||||
```
|
||||
|
||||
This will output the following two lines:
|
||||
```
|
||||
Message 1: Hi, here is my first message!
|
||||
Message 2: Got it!
|
||||
```
|
||||
|
||||
<!-- TODO: ### Clean shutdown? - seems to be more advanced to call async functions on exit, also is this needed in this usecase? -->
|
||||
|
||||
## Further information
|
||||
|
||||
- `@deltachat/stdio-rpc-server`
|
||||
- [package on npm](https://www.npmjs.com/package/@deltachat/stdio-rpc-server)
|
||||
- [source code on github](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package)
|
||||
- [use `@deltachat/stdio-rpc-server` on an usuported platform](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package#how-to-use-on-an-unsupported-platform)
|
||||
- The issue-tracker for the core library is here: <https://github.com/deltachat/deltachat-core-rust/issues>
|
||||
|
||||
If you need further assistance,
|
||||
please do not hesitate to contact us
|
||||
through the channels shown at https://delta.chat/en/contribute
|
||||
|
||||
Please keep in mind, that your derived work
|
||||
must respect the Mozilla Public License 2.0 of deltachat-rpc-server
|
||||
and the respective licenses of the libraries deltachat-rpc-server links with.
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DcEvent, DeltaChat } from "../deltachat.js";
|
||||
import { DcEvent, WebsocketDeltaChat as DeltaChat } from "../deltachat.js";
|
||||
|
||||
var SELECTED_ACCOUNT = 0;
|
||||
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.147.1"
|
||||
"version": "1.148.6"
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { RawClient } from "../generated/client.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
export type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
export type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
) => void;
|
||||
@@ -83,7 +83,7 @@ export const DEFAULT_OPTS: Opts = {
|
||||
url: "ws://localhost:20808/ws",
|
||||
startEventLoop: true,
|
||||
};
|
||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
export class WebsocketDeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
opts: Opts;
|
||||
close() {
|
||||
this.transport.close();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -11,6 +11,7 @@ deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#![allow(clippy::format_push_string)]
|
||||
extern crate dirs;
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -24,6 +22,7 @@ use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
@@ -427,6 +426,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
createqrsvg <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
@@ -491,12 +491,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let provider = BackupProvider::prepare(&context).await?;
|
||||
let qr = format_backup(&provider.qr())?;
|
||||
println!("QR code: {}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
std::io::stdout().write_all(&output.stdout).unwrap();
|
||||
std::io::stderr().write_all(&output.stderr).unwrap();
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
provider.await?;
|
||||
}
|
||||
"receive-backup" => {
|
||||
@@ -1009,8 +1004,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
if !arg1.is_empty() {
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(arg1.to_string());
|
||||
let mut draft = Message::new_text(arg1.to_string());
|
||||
sel_chat
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -1033,8 +1027,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
!arg1.is_empty(),
|
||||
"Please specify text to add as device message."
|
||||
);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(arg1.to_string());
|
||||
let mut msg = Message::new_text(arg1.to_string());
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
"listmedia" => {
|
||||
@@ -1256,6 +1249,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let svg = create_qr_svg(arg1)?;
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
fs::write(&file, svg).await?;
|
||||
println!("{file:#?} written.");
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
let proxy_enabled = context
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
@@ -242,12 +240,13 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"createqrsvg",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
@@ -450,12 +449,7 @@ async fn handle_cmd(
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
let output = Command::new("qrencode")
|
||||
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -63,6 +63,7 @@ class EventType(str, Enum):
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
|
||||
@@ -7,6 +7,7 @@ If you want to debug iroh at rust-trace/log level set
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@@ -107,13 +108,15 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
|
||||
# Test that 128 KB of data can be sent in a single message.
|
||||
data = os.urandom(128000)
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == list(b"foo")
|
||||
assert event.data == list(data)
|
||||
break
|
||||
|
||||
|
||||
@@ -208,3 +211,28 @@ def test_no_reordering(acfactory, path_to_webxdc):
|
||||
if event.data[0] == i:
|
||||
break
|
||||
pytest.fail("Reordering detected")
|
||||
|
||||
|
||||
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
|
||||
|
||||
ac1_ac2_chat.send_text("Hello!")
|
||||
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
||||
ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot()
|
||||
assert ac2_hello_msg_snapshot.text == "Hello!"
|
||||
ac2_hello_msg_snapshot.chat.accept()
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
while 1:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
break
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -44,13 +45,6 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
# Test that email address is in SVG
|
||||
# when we have no display name.
|
||||
# Check only the domain name, because
|
||||
# long address may be split over multiple lines
|
||||
# and not matched.
|
||||
assert domain in svg
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
|
||||
# Test that display name is used
|
||||
@@ -332,7 +326,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
@@ -342,6 +335,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
@@ -465,7 +460,10 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# ac1new is only used to get a new address.
|
||||
ac1new = acfactory.new_preconfigured_account()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
@@ -474,6 +472,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("sending first message")
|
||||
msg_out = chat.send_text("old address").get_snapshot()
|
||||
@@ -571,6 +570,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
@@ -585,17 +585,29 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
# Loop sending message from ac1 to ac2
|
||||
# until ac2 accepts new ac1 key.
|
||||
#
|
||||
# This may not happen immediately because resetup of ac1
|
||||
# rewinds "smeared timestamp" so Date: header for messages
|
||||
# sent by new ac1 are in the past compared to the last Date:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
logging.info("ac2 received Hello!")
|
||||
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
||||
if not ac2_contact_ac1.get_snapshot().is_verified:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,20 +18,46 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close()
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close();
|
||||
}
|
||||
main()
|
||||
main();
|
||||
```
|
||||
|
||||
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
|
||||
For a more complete example refer to https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc.
|
||||
|
||||
## How to use on an unsupported platform
|
||||
|
||||
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
|
||||
You need to have rust installed to compile deltachat core for your platform and cpu architecture.
|
||||
<https://rustup.rs/> is the recommended way to install rust.
|
||||
Also your system probably needs more than 4gb ram to compile core, alternatively your could try to build the debug build, that might take less ram to build.
|
||||
|
||||
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
|
||||
1. clone the core repo, right next to your project folder: `git clone git@github.com:deltachat/deltachat-core-rust.git`
|
||||
2. go into your core checkout and run `git pull` and `git checkout <version>` to point it to the correct version (needs to be the same version the `@deltachat/jsonrpc-client` package has)
|
||||
3. run `cargo build --release --package deltachat-rpc-server --bin deltachat-rpc-server`
|
||||
|
||||
Then you have 2 options:
|
||||
|
||||
### point to deltachat-rpc-server via direct path:
|
||||
|
||||
```sh
|
||||
# start your app with the DELTA_CHAT_RPC_SERVER env var
|
||||
DELTA_CHAT_RPC_SERVER="../deltachat-core-rust/target/release/deltachat-rpc-server" node myapp.js
|
||||
```
|
||||
|
||||
### install deltachat-rpc-server in your $PATH:
|
||||
|
||||
```sh
|
||||
# use this to install to ~/.cargo/bin
|
||||
cargo install --release --package deltachat-rpc-server --bin deltachat-rpc-server
|
||||
# or manually move deltachat-core-rust/target/release/deltachat-rpc-server
|
||||
# to a location that is included in your $PATH Environment variable.
|
||||
```
|
||||
|
||||
```js
|
||||
startDeltaChat("data-dir", { takeVersionFromPATH: true });
|
||||
```
|
||||
|
||||
## How does it work when you install it
|
||||
|
||||
@@ -46,7 +72,7 @@ references:
|
||||
When you import this package it searches for the rpc server in the following locations and order:
|
||||
|
||||
1. `DELTA_CHAT_RPC_SERVER` environment variable
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
3. prebuilds in npm packages
|
||||
|
||||
so by default it uses the prebuilds.
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.147.1"
|
||||
"version": "1.148.6"
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -195,11 +195,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1714076141,
|
||||
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
|
||||
"lastModified": 1729413321,
|
||||
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
|
||||
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -50,6 +50,7 @@ module.exports = {
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
@@ -67,6 +68,7 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT: 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -38,6 +39,7 @@ module.exports = {
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
|
||||
@@ -50,6 +50,7 @@ export enum C {
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
|
||||
DC_EVENT_INCOMING_REACTION = 2002,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
@@ -67,6 +68,7 @@ export enum C {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
@@ -321,6 +323,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -343,6 +346,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
|
||||
+1
-1
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.147.1"
|
||||
"version": "1.148.6"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.147.1"
|
||||
version = "1.148.6"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -308,7 +308,7 @@ class Chat:
|
||||
msg = as_dc_charpointer(text)
|
||||
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be send, does chat exist?")
|
||||
raise ValueError("The message could not be sent. Does the chat exist?")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def send_file(self, path, mime_type="application/octet-stream"):
|
||||
|
||||
@@ -2209,6 +2209,19 @@ def test_configure_error_msgs_wrong_pw(acfactory):
|
||||
# Password is wrong so it definitely has to say something about "password"
|
||||
assert "password" in ev.data2
|
||||
|
||||
ac1.stop_io()
|
||||
ac1.set_config("mail_pw", "abc") # Wrong mail pw
|
||||
ac1.configure()
|
||||
while True:
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||
print(f"Configuration progress: {ev.data1}")
|
||||
if ev.data1 == 0:
|
||||
break
|
||||
assert "password" in ev.data2
|
||||
# Account will continue to work with the old password, so if it becomes wrong, a notification
|
||||
# must be shown.
|
||||
assert ac1.get_config("notify_about_wrong_pw") == "1"
|
||||
|
||||
|
||||
def test_configure_error_msgs_invalid_server(acfactory):
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2024-10-13
|
||||
2024-10-31
|
||||
@@ -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.81.0
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
+45
-18
@@ -5,7 +5,8 @@ use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use futures::future::join_all;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -301,20 +302,48 @@ impl Accounts {
|
||||
///
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_without_timeout(&self) {
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
async fn background_fetch_and_log_error(account: Context) {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
}
|
||||
|
||||
join_all(
|
||||
self.accounts
|
||||
.values()
|
||||
.cloned()
|
||||
.map(background_fetch_and_log_error),
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
)),
|
||||
});
|
||||
let mut futures_unordered: FuturesUnordered<_> = accounts
|
||||
.into_iter()
|
||||
.map(background_fetch_and_log_error)
|
||||
.collect();
|
||||
while futures_unordered.next().await.is_some() {}
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
async fn background_fetch_with_timeout(
|
||||
accounts: Vec<Context>,
|
||||
events: Events,
|
||||
timeout: std::time::Duration,
|
||||
) {
|
||||
if let Err(_err) = tokio::time::timeout(
|
||||
timeout,
|
||||
Self::background_fetch_no_timeout(accounts, events.clone()),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Warning("Background fetch timed out.".to_string()),
|
||||
});
|
||||
}
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::AccountsBackgroundFetchDone,
|
||||
});
|
||||
}
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
@@ -322,15 +351,13 @@ impl Accounts {
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
pub async fn background_fetch(&self, timeout: std::time::Duration) {
|
||||
if let Err(_err) =
|
||||
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
|
||||
{
|
||||
self.emit_event(EventType::Warning(
|
||||
"Background fetch timed out.".to_string(),
|
||||
));
|
||||
}
|
||||
self.emit_event(EventType::AccountsBackgroundFetchDone);
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
Self::background_fetch_with_timeout(accounts, events, timeout)
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
@@ -344,7 +371,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Sets notification token for Apple Push Notification service.
|
||||
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
|
||||
pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -622,7 +622,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BlobObject<'a> {
|
||||
impl fmt::Display for BlobObject<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "$BLOBDIR/{}", self.name)
|
||||
}
|
||||
|
||||
+168
-117
@@ -578,7 +578,7 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
/// Sets protection and adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
/// and should be the timestamp of the change happening.
|
||||
@@ -589,20 +589,16 @@ impl ChatId {
|
||||
timestamp_sort: i64,
|
||||
contact_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
match self.inner_set_protection(context, protect).await {
|
||||
Ok(protection_status_modified) => {
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
|
||||
Err(e)
|
||||
}
|
||||
let protection_status_modified = self
|
||||
.inner_set_protection(context, protect)
|
||||
.await
|
||||
.with_context(|| format!("Cannot set protection for {self}"))?;
|
||||
if protection_status_modified {
|
||||
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
|
||||
.await?;
|
||||
chatlist_events::emit_chatlist_item_changed(context, self);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
@@ -616,8 +612,9 @@ impl ChatId {
|
||||
contact_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts = self
|
||||
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, false)
|
||||
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming)
|
||||
.await?
|
||||
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
|
||||
// in case of race conditions.
|
||||
@@ -800,8 +797,7 @@ impl ChatId {
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::self_deleted_msg_body(context).await;
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
@@ -869,13 +865,14 @@ impl ChatId {
|
||||
///
|
||||
/// Returns `true`, if message was deleted, `false` otherwise.
|
||||
async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
|
||||
match self.get_draft_msg_id(context).await? {
|
||||
Some(msg_id) => {
|
||||
msg_id.delete_from_db(context).await?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
Ok(context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
> 0)
|
||||
}
|
||||
|
||||
/// Set provided message as draft message for specified chat.
|
||||
@@ -947,12 +944,18 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
|
||||
// insert new draft
|
||||
self.maybe_delete_draft(context).await?;
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs (
|
||||
.transaction(|transaction| {
|
||||
// Delete existing draft if it exists.
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
)?;
|
||||
|
||||
// Insert new draft.
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
chat_id,
|
||||
from_id,
|
||||
timestamp,
|
||||
@@ -964,19 +967,22 @@ impl ChatId {
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(transaction.last_insert_rowid())
|
||||
})
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
Ok(true)
|
||||
@@ -1392,12 +1398,14 @@ impl ChatId {
|
||||
/// corresponding event in case of a system message (usually the current system time).
|
||||
/// `always_sort_to_bottom` makes this ajust the returned timestamp up so that the message goes
|
||||
/// to the chat bottom.
|
||||
/// `received` -- whether the message is received. Otherwise being sent.
|
||||
/// `incoming` -- whether the message is incoming.
|
||||
pub(crate) async fn calc_sort_timestamp(
|
||||
self,
|
||||
context: &Context,
|
||||
message_timestamp: i64,
|
||||
always_sort_to_bottom: bool,
|
||||
received: bool,
|
||||
incoming: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
|
||||
@@ -1418,25 +1426,38 @@ impl ChatId {
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?
|
||||
} else if incoming {
|
||||
// get newest non fresh message for this chat.
|
||||
|
||||
// If a user hasn't been online for some time, the Inbox is fetched first and then the
|
||||
// Sentbox. In order for Inbox and Sent messages to be allowed to mingle, outgoing
|
||||
// messages are purely sorted by their sent timestamp. NB: The Inbox must be fetched
|
||||
// first otherwise Inbox messages would be always below old Sentbox messages. We could
|
||||
// take in the query below only incoming messages, but then new incoming messages would
|
||||
// mingle with just sent outgoing ones and apear somewhere in the middle of the chat.
|
||||
} else if received {
|
||||
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
|
||||
// middle of the chat, so we go after the newest non fresh message.
|
||||
//
|
||||
// But if a received outgoing message is older than some seen message, better sort the
|
||||
// received message purely by timestamp. We could place it just before that seen
|
||||
// message, but anyway the user may not notice it.
|
||||
//
|
||||
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
|
||||
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
|
||||
// fresh ones, they rather mean that older incoming messages are actually seen as well.
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp)
|
||||
.query_row_optional(
|
||||
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
|
||||
FROM msgs
|
||||
WHERE chat_id=? AND hidden=0 AND state>?
|
||||
HAVING COUNT(*) > 0",
|
||||
(self, MessageState::InFresh),
|
||||
(MessageState::InSeen, self, MessageState::InFresh),
|
||||
|row| {
|
||||
let ts: i64 = row.get(0)?;
|
||||
let ts_sent_seen: i64 = row.get(1)?;
|
||||
Ok((ts, ts_sent_seen))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.and_then(|(ts, ts_sent_seen)| {
|
||||
match incoming || ts_sent_seen <= message_timestamp {
|
||||
true => Some(ts),
|
||||
false => None,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2070,28 +2091,31 @@ impl Chat {
|
||||
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
|
||||
};
|
||||
|
||||
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
|
||||
let new_mime_headers = if msg.has_html() {
|
||||
let html = if msg.param.exists(Param::Forwarded) {
|
||||
if msg.param.exists(Param::Forwarded) {
|
||||
msg.get_id().get_html(context).await?
|
||||
} else {
|
||||
msg.param.get(Param::SendHtml).map(|s| s.to_string())
|
||||
};
|
||||
match html {
|
||||
Some(html) => Some(tokio::task::block_in_place(move || {
|
||||
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
|
||||
})?),
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
|
||||
true => Some(msg.text.clone()),
|
||||
false => None,
|
||||
});
|
||||
let new_mime_headers = match new_mime_headers {
|
||||
Some(h) => Some(tokio::task::block_in_place(move || {
|
||||
buf_compress(new_html_mimepart(h).build().as_string().as_bytes())
|
||||
})?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
msg.chat_id = self.id;
|
||||
msg.from_id = ContactId::SELF;
|
||||
msg.rfc724_mid = new_rfc724_mid;
|
||||
msg.timestamp_sort = timestamp;
|
||||
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
|
||||
let mime_modified = new_mime_headers.is_some() | was_truncated;
|
||||
|
||||
// add message to the database
|
||||
if let Some(update_msg_id) = update_msg_id {
|
||||
@@ -2120,7 +2144,7 @@ impl Chat {
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
mime_modified,
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
@@ -2171,7 +2195,7 @@ impl Chat {
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
mime_modified,
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
@@ -2919,7 +2943,10 @@ async fn prepare_send_msg(
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
create_send_msg_jobs(context, msg).await
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
||||
@@ -3078,8 +3105,7 @@ pub async fn send_text_msg(
|
||||
chat_id
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = text_to_send;
|
||||
let mut msg = Message::new_text(text_to_send);
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
@@ -3455,6 +3481,29 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a vector of contact IDs for given chat ID where the contact is not SELF.
|
||||
pub async fn get_other_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
|
||||
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
|
||||
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
|
||||
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT cc.contact_id
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=? AND c.id!=1
|
||||
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
|
||||
(chat_id,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Creates a group chat with a given `name`.
|
||||
pub async fn create_group_chat(
|
||||
context: &Context,
|
||||
@@ -4752,8 +4801,7 @@ mod tests {
|
||||
async fn test_get_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hello".to_string());
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
let draft = chat_id.get_draft(&t).await.unwrap().unwrap();
|
||||
@@ -4767,13 +4815,11 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi!".to_string());
|
||||
let mut msg = Message::new_text("hi!".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("another".to_string());
|
||||
let mut msg = Message::new_text("another".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
@@ -4787,8 +4833,7 @@ mod tests {
|
||||
async fn test_forwarding_draft_failing() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hello".to_string());
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await?;
|
||||
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
|
||||
|
||||
@@ -4801,8 +4846,7 @@ mod tests {
|
||||
async fn test_draft_stable_ids() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = &t.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hello".to_string());
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
assert_eq!(msg.id, MsgId::new_unset());
|
||||
assert!(chat_id.get_draft_msg_id(&t).await?.is_none());
|
||||
|
||||
@@ -4842,6 +4886,33 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_only_one_draft_per_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let msgs: Vec<message::Message> = (1..=1000)
|
||||
.map(|i| Message::new_text(i.to_string()))
|
||||
.collect();
|
||||
let mut tasks = Vec::new();
|
||||
for mut msg in msgs {
|
||||
let ctx = t.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let ctx = ctx;
|
||||
chat_id.set_draft(&ctx, Some(&mut msg)).await
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
futures::future::join_all(tasks.into_iter()).await;
|
||||
|
||||
assert!(chat_id.get_draft(&t).await?.is_some());
|
||||
|
||||
chat_id.set_draft(&t, None).await?;
|
||||
assert!(chat_id.get_draft(&t).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -4854,8 +4925,7 @@ mod tests {
|
||||
.await?;
|
||||
|
||||
// save a draft
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text("draft text".to_string());
|
||||
let mut draft = Message::new_text("draft text".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
@@ -4908,29 +4978,25 @@ mod tests {
|
||||
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
|
||||
|
||||
// quoting messages in same chat is okay
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&grp_msg)).await?;
|
||||
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&one2one_msg)).await?;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
let one2one_quote_reply_msg_id = result.unwrap();
|
||||
|
||||
// quoting messages from groups to one-to-ones is okay ("reply privately")
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&grp_msg)).await?;
|
||||
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("baz".to_string());
|
||||
let mut msg = Message::new_text("baz".to_string());
|
||||
msg.set_quote(&alice, Some(&one2one_msg)).await?;
|
||||
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
|
||||
assert!(result.is_err());
|
||||
@@ -5422,13 +5488,11 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// add two device-messages
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_text("first message".to_string());
|
||||
let mut msg1 = Message::new_text("first message".to_string());
|
||||
let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await;
|
||||
assert!(msg1_id.is_ok());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_text("second message".to_string());
|
||||
let mut msg2 = Message::new_text("second message".to_string());
|
||||
let msg2_id = add_device_msg(&t, None, Some(&mut msg2)).await;
|
||||
assert!(msg2_id.is_ok());
|
||||
assert_ne!(msg1_id.as_ref().unwrap(), msg2_id.as_ref().unwrap());
|
||||
@@ -5457,14 +5521,12 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// add two device-messages with the same label (second attempt is not added)
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.text = "first message".to_string();
|
||||
let mut msg1 = Message::new_text("first message".to_string());
|
||||
let msg1_id = add_device_msg(&t, Some("any-label"), Some(&mut msg1)).await;
|
||||
assert!(msg1_id.is_ok());
|
||||
assert!(!msg1_id.as_ref().unwrap().is_unset());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.text = "second message".to_string();
|
||||
let mut msg2 = Message::new_text("second message".to_string());
|
||||
let msg2_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
|
||||
assert!(msg2_id.is_ok());
|
||||
assert!(msg2_id.as_ref().unwrap().is_unset());
|
||||
@@ -5511,8 +5573,7 @@ mod tests {
|
||||
let res = add_device_msg(&t, Some("some-label"), None).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("message text".to_string());
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
|
||||
let msg_id = add_device_msg(&t, Some("some-label"), Some(&mut msg)).await;
|
||||
assert!(msg_id.is_ok());
|
||||
@@ -5529,8 +5590,7 @@ mod tests {
|
||||
add_device_msg(&t, Some("some-label"), None).await.ok();
|
||||
assert!(was_device_msg_ever_added(&t, "some-label").await.unwrap());
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("message text".to_string());
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
add_device_msg(&t, Some("another-label"), Some(&mut msg))
|
||||
.await
|
||||
.ok();
|
||||
@@ -5547,8 +5607,7 @@ mod tests {
|
||||
async fn test_delete_device_chat() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("message text".to_string());
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
add_device_msg(&t, Some("some-label"), Some(&mut msg))
|
||||
.await
|
||||
.ok();
|
||||
@@ -5571,8 +5630,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("message text".to_string());
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
assert!(send_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
assert!(prepare_msg(&t, device_chat_id, &mut msg).await.is_err());
|
||||
|
||||
@@ -5583,8 +5641,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_and_reset_all_device_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("message text".to_string());
|
||||
let mut msg = Message::new_text("message text".to_string());
|
||||
let msg_id1 = add_device_msg(&t, Some("some-label"), Some(&mut msg))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5616,8 +5673,7 @@ mod tests {
|
||||
async fn test_archive() {
|
||||
// create two chats
|
||||
let t = TestContext::new().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("foo".to_string());
|
||||
let mut msg = Message::new_text("foo".to_string());
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t, msg_id)
|
||||
.await
|
||||
@@ -5917,8 +5973,7 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("foo".to_string());
|
||||
let mut msg = Message::new_text("foo".to_string());
|
||||
let msg_id = add_device_msg(&t, None, Some(&mut msg)).await.unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t, msg_id)
|
||||
.await
|
||||
@@ -5995,8 +6050,7 @@ mod tests {
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi!".into());
|
||||
let mut msg = Message::new_text("hi!".into());
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
@@ -6635,8 +6689,7 @@ mod tests {
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("Hi Bob".to_owned());
|
||||
let mut msg = Message::new_text("Hi Bob".to_owned());
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
@@ -6687,8 +6740,7 @@ mod tests {
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text("Reply".to_owned());
|
||||
let mut reply = Message::new_text("Reply".to_owned());
|
||||
reply.set_quote(&bob, Some(&received_msg)).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
let received_reply = alice.recv_msg(&sent_reply).await;
|
||||
@@ -6771,8 +6823,7 @@ mod tests {
|
||||
let group_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
|
||||
add_contact_to_chat(&alice, group_id, bob_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("bla foo".to_owned());
|
||||
let mut msg = Message::new_text("bla foo".to_owned());
|
||||
let sent_msg = alice.send_msg(group_id, &mut msg).await;
|
||||
assert!(sent_msg.payload().contains("secretgrpname"));
|
||||
assert!(sent_msg.payload().contains("secretname"));
|
||||
|
||||
+2
-5
@@ -476,7 +476,6 @@ mod tests {
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
};
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
@@ -510,8 +509,7 @@ mod tests {
|
||||
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hello".to_string());
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -755,8 +753,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("foo:\nbar \r\n test".to_string());
|
||||
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
|
||||
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -321,6 +321,10 @@ pub enum Config {
|
||||
/// True if account is muted.
|
||||
IsMuted,
|
||||
|
||||
/// Optional tag as "Work", "Family".
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
PrivateTag,
|
||||
|
||||
/// All secondary self addresses separated by spaces
|
||||
/// (`addr1@example.org addr2@example.org addr3@example.org`)
|
||||
SecondaryAddrs,
|
||||
@@ -429,6 +433,7 @@ pub enum Config {
|
||||
WebxdcIntegration,
|
||||
|
||||
/// Enable webxdc realtime features.
|
||||
#[strum(props(default = "1"))]
|
||||
WebxdcRealtimeEnabled,
|
||||
}
|
||||
|
||||
|
||||
+20
-16
@@ -19,11 +19,12 @@ use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
@@ -31,7 +32,7 @@ use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
|
||||
};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
@@ -111,15 +112,10 @@ impl Context {
|
||||
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
|
||||
let configured_param_res = configure(self, ¶m).await;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await?;
|
||||
|
||||
on_configure_completed(self, configured_param_res?, old_addr).await?;
|
||||
|
||||
let configured_param = configure(self, ¶m).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, configured_param, old_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -147,8 +143,7 @@ async fn on_configure_completed(
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = provider.after_login_hint.to_string();
|
||||
let mut msg = Message::new_text(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
@@ -161,9 +156,9 @@ async fn on_configure_completed(
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Some(old_addr) = old_addr {
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text =
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
|
||||
let mut msg = Message::new_text(
|
||||
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
|
||||
);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.context("Cannot add AEAP explanation")
|
||||
@@ -417,7 +412,8 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
configured_param.oauth2,
|
||||
r,
|
||||
);
|
||||
let mut imap_session = match imap.connect(ctx).await {
|
||||
let configuring = true;
|
||||
let mut imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
|
||||
};
|
||||
@@ -503,7 +499,15 @@ async fn get_autoconfig(
|
||||
param: &EnteredLoginParam,
|
||||
param_domain: &str,
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
// Make sure to not encode `.` as `%2E` here.
|
||||
// Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
|
||||
// when address is encoded.
|
||||
// E.g.
|
||||
// <https://autoconfig.murena.io/mail/config-v1.1.xml?emailaddress=foobar%40example%2Eorg>
|
||||
// produced XML file with `<username>foobar@example%2Eorg</username>`
|
||||
// resulting in failure to log in.
|
||||
let param_addr_urlencoded =
|
||||
utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
|
||||
+7
-1
@@ -4,12 +4,16 @@
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
@@ -179,7 +183,9 @@ pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEX
|
||||
// and may be set together with the username, password etc.
|
||||
// via dc_set_config() using the key "server_flags".
|
||||
|
||||
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
|
||||
/// Force OAuth2 authorization.
|
||||
///
|
||||
/// This flag does not skip automatic configuration.
|
||||
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
|
||||
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
||||
|
||||
+4
-1
@@ -425,9 +425,12 @@ pub enum Origin {
|
||||
/// To: of incoming messages of unknown sender
|
||||
IncomingUnknownTo = 0x40,
|
||||
|
||||
/// address scanned but not verified
|
||||
/// Address scanned but not verified.
|
||||
UnhandledQrScan = 0x80,
|
||||
|
||||
/// Address scanned from a SecureJoin QR code, but not verified yet.
|
||||
UnhandledSecurejoinQrScan = 0x81,
|
||||
|
||||
/// Reply-To: of incoming message of known sender
|
||||
/// Contacts with at least this origin value are shown in the contact list.
|
||||
IncomingReplyTo = 0x100,
|
||||
|
||||
+11
-9
@@ -28,7 +28,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::Iroh;
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -857,6 +857,12 @@ impl Context {
|
||||
"is_muted",
|
||||
self.get_config_bool(Config::IsMuted).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"private_tag",
|
||||
self.get_config(Config::PrivateTag)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
);
|
||||
|
||||
if let Some(metadata) = &*self.metadata.read().await {
|
||||
if let Some(comment) = &metadata.comment {
|
||||
@@ -1172,8 +1178,7 @@ impl Context {
|
||||
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
|
||||
.await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = self.get_self_report().await?;
|
||||
let mut msg = Message::new_text(self.get_self_report().await?);
|
||||
|
||||
chat_id.set_draft(self, Some(&mut msg)).await?;
|
||||
|
||||
@@ -1772,12 +1777,10 @@ mod tests {
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_text("foobar".to_string());
|
||||
let mut msg1 = Message::new_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_text("barbaz".to_string());
|
||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
@@ -1880,8 +1883,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("foobar".to_string());
|
||||
let mut msg = Message::new_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
|
||||
+1
-1
@@ -313,7 +313,7 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
if allow_change {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.apply_header(context, header, message_time);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
} else {
|
||||
info!(
|
||||
|
||||
+4
-3
@@ -318,8 +318,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("Hi Bob".to_owned());
|
||||
let mut msg = Message::new_text("Hi Bob".to_owned());
|
||||
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
@@ -335,7 +334,9 @@ mod tests {
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), *s);
|
||||
}
|
||||
msg_id.delete_from_db(&t).await?;
|
||||
t.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?", (msg_id,))
|
||||
.await?;
|
||||
// Nothing to do is ok.
|
||||
msg_id
|
||||
.update_download_state(&t, DownloadState::Done)
|
||||
|
||||
+5
-6
@@ -223,8 +223,9 @@ impl ChatId {
|
||||
self.inner_set_ephemeral_timer(context, timer).await?;
|
||||
|
||||
if self.is_promoted(context).await? {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
|
||||
let mut msg = Message::new_text(
|
||||
stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await,
|
||||
);
|
||||
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
|
||||
if let Err(err) = send_msg(context, self, &mut msg).await {
|
||||
error!(
|
||||
@@ -1362,8 +1363,7 @@ mod tests {
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi".to_string());
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
@@ -1393,8 +1393,7 @@ mod tests {
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let mut poi_msg = Message::new(Viewtype::Text);
|
||||
poi_msg.text = "Here".to_string();
|
||||
let mut poi_msg = Message::new_text("Here".to_string());
|
||||
poi_msg.set_location(10.0, 20.0);
|
||||
|
||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
use crate::reaction::Reaction;
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
/// Event payload.
|
||||
@@ -94,6 +95,18 @@ pub enum EventType {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
IncomingReaction {
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
|
||||
/// ID of the message for which reactions were changed.
|
||||
msg_id: MsgId,
|
||||
|
||||
/// The reaction.
|
||||
reaction: Reaction,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -288,6 +301,13 @@ pub enum EventType {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Advertisement received over an ephemeral peer channel.
|
||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
WebxdcRealtimeAdvertisementReceived {
|
||||
/// Message ID of the webxdc instance.
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted.
|
||||
WebxdcInstanceDeleted {
|
||||
/// ID of the deleted message.
|
||||
|
||||
+1
-2
@@ -525,8 +525,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// alice sends a message with html-part to bob
|
||||
let chat_id = alice.create_chat(&bob).await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("plain text".to_string());
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
assert!(msg.mime_modified);
|
||||
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
|
||||
|
||||
+62
-33
@@ -36,7 +36,7 @@ use crate::log::LogExt;
|
||||
use crate::login_param::{
|
||||
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
@@ -290,7 +290,11 @@ impl Imap {
|
||||
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
|
||||
/// instead if you are going to actually use connection rather than trying connection
|
||||
/// parameters.
|
||||
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
|
||||
pub(crate) async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
configuring: bool,
|
||||
) -> Result<Session> {
|
||||
let now = tools::Time::now();
|
||||
let until_can_send = max(
|
||||
min(self.conn_last_try, now)
|
||||
@@ -416,21 +420,13 @@ impl Imap {
|
||||
warn!(context, "IMAP failed to login: {err:#}.");
|
||||
first_error.get_or_insert(format_err!("{message} ({err:#})"));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
let _lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if !configuring
|
||||
&& self.login_failed_once
|
||||
&& err_str.to_lowercase().contains("authentication")
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
if let Err(e) = context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{e:#}.");
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text.clone_from(&message);
|
||||
let mut msg = Message::new_text(message);
|
||||
if let Err(e) = chat::add_device_msg_with_importance(
|
||||
context,
|
||||
None,
|
||||
@@ -440,6 +436,12 @@ impl Imap {
|
||||
.await
|
||||
{
|
||||
warn!(context, "Failed to add device message: {e:#}.");
|
||||
} else {
|
||||
context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
@@ -456,7 +458,8 @@ impl Imap {
|
||||
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
|
||||
/// determined.
|
||||
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
|
||||
let mut session = match self.connect(context).await {
|
||||
let configuring = false;
|
||||
let mut session = match self.connect(context, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
self.connectivity.set_err(context, &err).await;
|
||||
@@ -1196,6 +1199,8 @@ impl Session {
|
||||
.await
|
||||
.context("failed to fetch flags")?;
|
||||
|
||||
let mut got_unsolicited_fetch = false;
|
||||
|
||||
while let Some(fetch) = list
|
||||
.try_next()
|
||||
.await
|
||||
@@ -1205,6 +1210,7 @@ impl Session {
|
||||
uid
|
||||
} else {
|
||||
info!(context, "FETCH result contains no UID, skipping");
|
||||
got_unsolicited_fetch = true;
|
||||
continue;
|
||||
};
|
||||
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
|
||||
@@ -1227,6 +1233,15 @@ impl Session {
|
||||
warn!(context, "FETCH result contains no MODSEQ");
|
||||
}
|
||||
}
|
||||
drop(list);
|
||||
|
||||
if got_unsolicited_fetch {
|
||||
// We got unsolicited FETCH, which means some flags
|
||||
// have been modified while our request was in progress.
|
||||
// We may or may not have these new flags as a part of the response,
|
||||
// so better skip next IDLE and do another round of flag synchronization.
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
set_modseq(context, folder, highest_modseq)
|
||||
.await
|
||||
@@ -1712,17 +1727,21 @@ impl Imap {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Return whether the server sent an unsolicited EXISTS response.
|
||||
/// Return whether the server sent an unsolicited EXISTS or FETCH response.
|
||||
///
|
||||
/// Drains all responses from `session.unsolicited_responses` in the process.
|
||||
/// If this returns `true`, this means that new emails arrived and you should
|
||||
/// fetch again, even if you just fetched.
|
||||
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
|
||||
///
|
||||
/// If this returns `true`, this means that new emails arrived
|
||||
/// or flags have been changed.
|
||||
/// In this case we may want to skip next IDLE and do a round
|
||||
/// of fetching new messages and synchronizing seen flags.
|
||||
fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
use UnsolicitedResponse::*;
|
||||
|
||||
let folder = self.selected_folder.as_deref().unwrap_or_default();
|
||||
let mut unsolicited_exists = false;
|
||||
let mut should_refetch = false;
|
||||
while let Ok(response) = self.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
Exists(_) => {
|
||||
@@ -1730,28 +1749,38 @@ impl Session {
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
|
||||
);
|
||||
unsolicited_exists = true;
|
||||
should_refetch = true;
|
||||
}
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them
|
||||
Expunge(_) | Recent(_) => {}
|
||||
Other(response_data)
|
||||
if matches!(
|
||||
response_data.parsed(),
|
||||
Response::Fetch { .. }
|
||||
| Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
}
|
||||
) => {}
|
||||
Other(ref response_data) => {
|
||||
match response_data.parsed() {
|
||||
Response::Fetch { .. } => {
|
||||
info!(
|
||||
context,
|
||||
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
|
||||
);
|
||||
should_refetch = true;
|
||||
}
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them.
|
||||
Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
} => {}
|
||||
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!(context, "{folder:?}: got unsolicited response {response:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(unsolicited_exists)
|
||||
Ok(should_refetch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ impl Client {
|
||||
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
|
||||
}
|
||||
};
|
||||
update_connection_history(context, "imap", host, port, host, time()).await?;
|
||||
Ok(client)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
|
||||
+5
-27
@@ -9,7 +9,6 @@ use tokio::time::timeout;
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::context::Context;
|
||||
use crate::imap::FolderMeaning;
|
||||
use crate::net::TIMEOUT;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
@@ -32,7 +31,7 @@ impl Session {
|
||||
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
if self.drain_unsolicited_responses(context)? {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
@@ -109,37 +108,16 @@ impl Imap {
|
||||
pub(crate) async fn fake_idle(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
watch_folder: String,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> Result<()> {
|
||||
let fake_idle_start_time = tools::Time::now();
|
||||
|
||||
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
|
||||
|
||||
// Loop until we are interrupted or until we fetch something.
|
||||
loop {
|
||||
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
|
||||
Err(_) => {
|
||||
// Let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
let res = self
|
||||
.fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
|
||||
.await?;
|
||||
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
info!(context, "Fake IDLE interrupted.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for 60 seconds or until we are interrupted.
|
||||
match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
|
||||
Err(_) => info!(context, "Fake IDLE finished."),
|
||||
Ok(_) => info!(context, "Fake IDLE interrupted."),
|
||||
}
|
||||
|
||||
info!(
|
||||
|
||||
@@ -66,21 +66,11 @@ impl Imap {
|
||||
&& folder_meaning != FolderMeaning::Drafts
|
||||
&& folder_meaning != FolderMeaning::Trash
|
||||
{
|
||||
// Drain leftover unsolicited EXISTS messages
|
||||
session.server_sent_unsolicited_exists(context)?;
|
||||
|
||||
loop {
|
||||
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
|
||||
.await
|
||||
.context("Can't fetch new msgs in scanned folder")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
|
||||
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
|
||||
if !session.server_sent_unsolicited_exists(context)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
|
||||
.await
|
||||
.context("Can't fetch new msgs in scanned folder")
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::message::Message;
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{create_id, time, TempPathGuard};
|
||||
@@ -200,8 +200,7 @@ impl BackupProvider {
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = backup_transfer_msg_body(&context).await;
|
||||
let mut msg = Message::new_text(backup_transfer_msg_body(&context).await);
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -369,6 +368,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chat::{get_chat_msgs, send_msg, ChatItem};
|
||||
use crate::message::Viewtype;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
use super::*;
|
||||
@@ -382,8 +382,7 @@ mod tests {
|
||||
|
||||
// Write a message in the self chat
|
||||
let self_chat = ctx0.get_self_chat().await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi there".to_string());
|
||||
let mut msg = Message::new_text("hi there".to_string());
|
||||
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
|
||||
|
||||
// Send an attachment in the self chat
|
||||
|
||||
+1
-2
@@ -290,8 +290,7 @@ pub async fn send_locations_to_chat(
|
||||
)
|
||||
.await?;
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::msg_location_enabled(context).await;
|
||||
let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
|
||||
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
|
||||
+16
-27
@@ -148,21 +148,6 @@ impl MsgId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
|
||||
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?;
|
||||
transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
|
||||
update_msg_state(context, self, MessageState::OutDelivered).await?;
|
||||
let chat_id: ChatId = context
|
||||
@@ -506,6 +491,15 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new message with Viewtype::Text.
|
||||
pub fn new_text(text: String) -> Self {
|
||||
Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads message with given ID from the database.
|
||||
///
|
||||
/// Returns an error if the message does not exist.
|
||||
@@ -1915,6 +1909,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
||||
|
||||
/// Estimates the number of messages that will be deleted
|
||||
/// by the options `delete_device_after` or `delete_server_after`.
|
||||
///
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
/// before actually enabling deletion of old messages.
|
||||
///
|
||||
@@ -2347,8 +2342,7 @@ mod tests {
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("Quoted message".to_string());
|
||||
let mut msg = Message::new_text("Quoted message".to_string());
|
||||
|
||||
// Prepare message for sending, so it gets a Message-Id.
|
||||
assert!(msg.rfc724_mid.is_empty());
|
||||
@@ -2414,9 +2408,8 @@ mod tests {
|
||||
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
|
||||
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
let mut msg = Message::new_text("unencrypted".to_string());
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg.set_text("unencrypted".to_string());
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
@@ -2474,8 +2467,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("bla blubb".to_string());
|
||||
let mut msg = Message::new_text("bla blubb".to_string());
|
||||
msg.set_override_sender_name(Some("over ride".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
@@ -2522,8 +2514,7 @@ mod tests {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("this is the text!".to_string());
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
@@ -2608,8 +2599,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// check outgoing messages states on sender side
|
||||
let mut alice_msg = Message::new(Viewtype::Text);
|
||||
alice_msg.set_text("hi!".to_string());
|
||||
let mut alice_msg = Message::new_text("hi!".to_string());
|
||||
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
|
||||
|
||||
alice_chat
|
||||
@@ -2792,8 +2782,7 @@ def hello():
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi".to_string());
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
+19
-17
@@ -20,6 +20,7 @@ use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
@@ -32,7 +33,6 @@ use crate::tools::{
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, remove_subject_prefix, time,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
use crate::{location, peer_channels};
|
||||
|
||||
// attachments of 25 mb brutto should work on the majority of providers
|
||||
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
|
||||
@@ -82,7 +82,10 @@ pub struct MimeFactory {
|
||||
/// as needed.
|
||||
references: String,
|
||||
|
||||
/// True if the message requests Message Disposition Notification
|
||||
/// using `Chat-Disposition-Notification-To` header.
|
||||
req_mdn: bool,
|
||||
|
||||
last_added_location_id: Option<u32>,
|
||||
|
||||
/// If the created mime-structure contains sync-items,
|
||||
@@ -352,7 +355,11 @@ impl MimeFactory {
|
||||
// beside key- and member-changes, force a periodic re-gossip.
|
||||
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
if time() >= gossiped_timestamp + gossip_period {
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
// 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)
|
||||
} else {
|
||||
Ok(false)
|
||||
@@ -610,7 +617,9 @@ impl MimeFactory {
|
||||
// because replies to "Disposition-Notification-To" are weird in many cases
|
||||
// eg. are just freetext and/or do not follow any standard.
|
||||
headers.push(Header::new(
|
||||
"Chat-Disposition-Notification-To".into(),
|
||||
HeaderDef::ChatDispositionNotificationTo
|
||||
.get_headername()
|
||||
.to_string(),
|
||||
self.from_addr.clone(),
|
||||
));
|
||||
}
|
||||
@@ -1378,8 +1387,7 @@ impl MimeFactory {
|
||||
let json = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
let topic = peer_channels::create_random_topic();
|
||||
headers.push(create_iroh_header(context, topic, msg.id).await?);
|
||||
headers.push(create_iroh_header(context, msg.id).await?);
|
||||
if let (Some(json), _) = context
|
||||
.render_webxdc_status_update_object(
|
||||
msg.id,
|
||||
@@ -1974,8 +1982,7 @@ mod tests {
|
||||
group_id: ChatId,
|
||||
quote: Option<&Message>,
|
||||
) -> Result<String> {
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text("Hi".to_string());
|
||||
let mut new_msg = Message::new_text("Hi".to_string());
|
||||
if let Some(q) = quote {
|
||||
new_msg.set_quote(t, Some(q)).await?;
|
||||
}
|
||||
@@ -2061,8 +2068,7 @@ mod tests {
|
||||
|
||||
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
|
||||
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text("Hi".to_string());
|
||||
let mut new_msg = Message::new_text("Hi".to_string());
|
||||
new_msg.chat_id = chat_id;
|
||||
chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
||||
|
||||
@@ -2169,8 +2175,7 @@ mod tests {
|
||||
let chat_id = chats.get_chat_id(0).unwrap();
|
||||
chat_id.accept(context).await.unwrap();
|
||||
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text("Hi".to_string());
|
||||
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
|
||||
@@ -2287,8 +2292,7 @@ mod tests {
|
||||
|
||||
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("this is the text!".to_string());
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
|
||||
@@ -2354,8 +2358,7 @@ mod tests {
|
||||
// send message to bob: that should get multipart/signed.
|
||||
// `Subject:` is protected by copying it.
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("this is the text!".to_string());
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
|
||||
@@ -2488,8 +2491,7 @@ mod tests {
|
||||
|
||||
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("this is the text!".to_string());
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let payload = sent_msg.payload();
|
||||
|
||||
+35
-4
@@ -770,6 +770,15 @@ impl MimeMessage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set different sender name for a message.
|
||||
/// This overrides the name set by the `set_config()`-option `displayname`.
|
||||
pub fn set_override_sender_name(&mut self, name: Option<String>) {
|
||||
self.parts.iter_mut().for_each(|part| {
|
||||
part.param
|
||||
.set_optional(Param::OverrideSenderDisplayname, name.clone());
|
||||
});
|
||||
}
|
||||
|
||||
async fn avatar_action_from_header(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -2240,12 +2249,22 @@ async fn handle_ndn(
|
||||
} 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, chat_id, chat_type) = msg?;
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
set_msg_failed(context, &mut message, &error).await?;
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| format!("{}\n\n{}", err, err_msg));
|
||||
set_msg_failed(
|
||||
context,
|
||||
&mut message,
|
||||
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?;
|
||||
@@ -3598,11 +3617,24 @@ On 2020-10-25, Bob wrote:
|
||||
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
|
||||
}
|
||||
|
||||
{
|
||||
for draft in [false, true] {
|
||||
let chat = t.get_self_chat().await;
|
||||
t.send_text(chat.id, &long_txt).await;
|
||||
let mut msg = Message::new_text(long_txt.clone());
|
||||
if draft {
|
||||
chat.id.set_draft(&t, Some(&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());
|
||||
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()
|
||||
);
|
||||
@@ -3610,7 +3642,6 @@ On 2020-10-25, Bob wrote:
|
||||
}
|
||||
|
||||
t.set_config(Config::Bot, Some("1")).await?;
|
||||
|
||||
{
|
||||
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?;
|
||||
assert!(!mimemsg.is_mime_modified);
|
||||
|
||||
@@ -47,6 +47,14 @@ pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the timestamp of the last successfull connection
|
||||
/// to the given `host` and `port`
|
||||
/// with the given application protocol `alpn`.
|
||||
///
|
||||
/// `addr` is the string representation of IP address.
|
||||
/// If connection is made over a proxy which does
|
||||
/// its own DNS resolution,
|
||||
/// `addr` should be the same as `host`.
|
||||
pub(crate) async fn update_connection_history(
|
||||
context: &Context,
|
||||
alpn: &str,
|
||||
|
||||
+289
-26
@@ -1,4 +1,44 @@
|
||||
//! DNS resolution and cache.
|
||||
//!
|
||||
//! DNS cache in Delta Chat has two layers:
|
||||
//! in-memory cache and persistent `dns_cache` SQL table.
|
||||
//!
|
||||
//! In-memory cache is using a "stale-while-revalidate" strategy.
|
||||
//! If there is a cached value, it is returned immediately
|
||||
//! and revalidation task is started in the background
|
||||
//! to replace old cached IP addresses with new ones.
|
||||
//! If there is no cached value yet,
|
||||
//! lookup only finishes when `lookup_host` returns first results.
|
||||
//! In-memory cache is shared between all accounts
|
||||
//! and is never stored on the disk.
|
||||
//! It can be thought of as an extension
|
||||
//! of the system resolver.
|
||||
//!
|
||||
//! Persistent `dns_cache` SQL table is used to collect
|
||||
//! all IP addresses ever seen for the hostname
|
||||
//! together with the timestamp
|
||||
//! of the last time IP address has been seen.
|
||||
//! Note that this timestamp reflects the time
|
||||
//! IP address was returned by the in-memory cache
|
||||
//! rather than the underlying system resolver.
|
||||
//! Unused entries are removed after 30 days
|
||||
//! (`CACHE_TTL` constant) to avoid having
|
||||
//! old non-working IP addresses in the cache indefinitely.
|
||||
//!
|
||||
//! When Delta Chat needs an IP address for the host,
|
||||
//! it queries in-memory cache for the next result
|
||||
//! and merges the list of IP addresses
|
||||
//! with the list of IP addresses from persistent cache.
|
||||
//! Resulting list is constructed
|
||||
//! by taking the first two results from the resolver
|
||||
//! followed up by persistent cache results
|
||||
//! and terminated by the rest of resolver results.
|
||||
//!
|
||||
//! Persistent cache results are sorted
|
||||
//! by the time of the most recent successful connection
|
||||
//! using the result. For results that have never been
|
||||
//! used for successful connection timestamp of
|
||||
//! retrieving them from in-memory cache is used.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use std::collections::HashMap;
|
||||
@@ -42,33 +82,110 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Looks up the hostname and updates DNS cache
|
||||
/// on success.
|
||||
/// Map from hostname to IP addresses.
|
||||
///
|
||||
/// NOTE: sync RwLock is used, so it must not be held across `.await`
|
||||
/// to avoid deadlocks.
|
||||
/// See
|
||||
/// <https://docs.rs/tokio/1.40.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
|
||||
/// and
|
||||
/// <https://stackoverflow.com/questions/63712823/why-do-i-get-a-deadlock-when-using-tokio-with-a-stdsyncmutex>.
|
||||
static LOOKUP_HOST_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
/// Wrapper for `lookup_host` that returns IP addresses.
|
||||
async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result<impl Iterator<Item = IpAddr>> {
|
||||
Ok(lookup_host(host)
|
||||
.await
|
||||
.context("DNS lookup failure")?
|
||||
.map(|addr| addr.ip()))
|
||||
}
|
||||
|
||||
async fn lookup_host_with_memory_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
) -> Result<Vec<IpAddr>> {
|
||||
let stale_result = {
|
||||
let rwlock_read_guard = LOOKUP_HOST_CACHE.read();
|
||||
rwlock_read_guard.get(hostname).cloned()
|
||||
};
|
||||
if let Some(stale_result) = stale_result {
|
||||
// Revalidate the cache in the background.
|
||||
{
|
||||
let context = context.clone();
|
||||
let hostname = hostname.to_string();
|
||||
tokio::spawn(async move {
|
||||
match lookup_ips((hostname.clone(), port)).await {
|
||||
Ok(res) => {
|
||||
LOOKUP_HOST_CACHE.write().insert(hostname, res.collect());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to revalidate results for {hostname:?}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Using memory-cached DNS resolution for {hostname}."
|
||||
);
|
||||
Ok(stale_result)
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"No memory-cached DNS resolution for {hostname} available, waiting for the resolver."
|
||||
);
|
||||
let res: Vec<IpAddr> = lookup_ips((hostname, port)).await?.collect();
|
||||
|
||||
// Insert initial result into the cache.
|
||||
//
|
||||
// There may already be a result from a parallel
|
||||
// task stored, overwriting it is not a problem.
|
||||
LOOKUP_HOST_CACHE
|
||||
.write()
|
||||
.insert(hostname.to_string(), res.clone());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up the hostname and updates
|
||||
/// persistent DNS cache on success.
|
||||
async fn lookup_host_and_update_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
now: i64,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let res: Vec<SocketAddr> = timeout(super::TIMEOUT, lookup_host((hostname, port)))
|
||||
.await
|
||||
.context("DNS lookup timeout")?
|
||||
.context("DNS lookup failure")?
|
||||
.collect();
|
||||
let res: Vec<IpAddr> = timeout(
|
||||
super::TIMEOUT,
|
||||
lookup_host_with_memory_cache(context, hostname, port),
|
||||
)
|
||||
.await
|
||||
.context("DNS lookup timeout")?
|
||||
.context("DNS lookup with memory cache failure")?;
|
||||
|
||||
for addr in &res {
|
||||
let ip_string = addr.ip().to_string();
|
||||
for ip in &res {
|
||||
let ip_string = ip.to_string();
|
||||
if ip_string == hostname {
|
||||
// IP address resolved into itself, not interesting to cache.
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(context, "Resolved {hostname}:{port} into {addr}.");
|
||||
info!(context, "Resolved {hostname} into {ip}.");
|
||||
|
||||
// Update the cache.
|
||||
update_cache(context, hostname, &ip_string, now).await?;
|
||||
}
|
||||
|
||||
let res = res
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -597,9 +714,14 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
load_cache: bool,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let now = time();
|
||||
let mut resolved_addrs = match lookup_host_and_update_cache(context, hostname, port, now).await
|
||||
{
|
||||
Ok(res) => res,
|
||||
let resolved_addrs = match lookup_host_and_update_cache(context, hostname, port, now).await {
|
||||
Ok(res) => {
|
||||
if alpn.is_empty() {
|
||||
res
|
||||
} else {
|
||||
sort_by_connection_timestamp(context, res, alpn, hostname).await?
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
@@ -608,29 +730,43 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !alpn.is_empty() {
|
||||
resolved_addrs =
|
||||
sort_by_connection_timestamp(context, resolved_addrs, alpn, hostname).await?;
|
||||
}
|
||||
|
||||
if load_cache {
|
||||
for addr in lookup_cache(context, hostname, port, alpn, now).await? {
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = lookup_cache(context, hostname, port, alpn, now).await?;
|
||||
if let Some(ips) = DNS_PRELOAD.get(hostname) {
|
||||
for ip in ips {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
if !cache.contains(&addr) {
|
||||
cache.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merge_with_cache(resolved_addrs, cache))
|
||||
} else {
|
||||
Ok(resolved_addrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges results received from DNS with cached results.
|
||||
///
|
||||
/// At most 10 results are returned.
|
||||
fn merge_with_cache(
|
||||
mut resolved_addrs: Vec<SocketAddr>,
|
||||
cache: Vec<SocketAddr>,
|
||||
) -> Vec<SocketAddr> {
|
||||
let rest = resolved_addrs.split_off(std::cmp::min(resolved_addrs.len(), 2));
|
||||
|
||||
for addr in cache.into_iter().chain(rest.into_iter()) {
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
if resolved_addrs.len() >= 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_addrs)
|
||||
resolved_addrs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -866,4 +1002,131 @@ mod tests {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_with_cache() {
|
||||
let first_addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
|
||||
let second_addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2));
|
||||
|
||||
// If there is no cache, just return resolved addresses.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
];
|
||||
let cache = vec![];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs.clone(), cache),
|
||||
resolved_addrs
|
||||
);
|
||||
}
|
||||
|
||||
// If cache contains address that is not in resolution results,
|
||||
// it is inserted in the merged result.
|
||||
{
|
||||
let resolved_addrs = vec![SocketAddr::new(first_addr, 993)];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// If cache contains address that is already in resolution results,
|
||||
// it is not duplicated.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(first_addr, 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// If DNS resolvers returns a lot of results,
|
||||
// we should try cached results before going through all
|
||||
// the resolver results.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
];
|
||||
let cache = vec![SocketAddr::new(second_addr, 993)];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Even if cache already contains all the incorrect results
|
||||
// that resolver returns, this should not result in them being sorted to the top.
|
||||
// Cache has known to work result returned first,
|
||||
// so we should try it after the second result.
|
||||
{
|
||||
let resolved_addrs = vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
];
|
||||
let cache = vec![
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
];
|
||||
assert_eq!(
|
||||
merge_with_cache(resolved_addrs, cache),
|
||||
vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 993),
|
||||
SocketAddr::new(second_addr, 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), 993),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), 993),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-2
@@ -12,13 +12,14 @@ use fast_socks5::client::Socks5Stream;
|
||||
use fast_socks5::util::target_addr::ToTargetAddr;
|
||||
use fast_socks5::AuthenticationMethod;
|
||||
use fast_socks5::Socks5Command;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use percent_encoding::{percent_encode, utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::net::session::SessionStream;
|
||||
@@ -41,6 +42,12 @@ impl PartialEq for ShadowsocksConfig {
|
||||
|
||||
impl Eq for ShadowsocksConfig {}
|
||||
|
||||
impl ShadowsocksConfig {
|
||||
fn to_url(&self) -> String {
|
||||
self.server_config.to_url()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HttpConfig {
|
||||
/// HTTP proxy host.
|
||||
@@ -84,6 +91,17 @@ impl HttpConfig {
|
||||
};
|
||||
Ok(http_config)
|
||||
}
|
||||
|
||||
fn to_url(&self, scheme: &str) -> String {
|
||||
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
|
||||
if let Some((user, password)) = &self.user_password {
|
||||
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
|
||||
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
|
||||
format!("{scheme}://{user}:{password}@{host}:{}", self.port)
|
||||
} else {
|
||||
format!("{scheme}://{host}:{}", self.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -123,6 +141,17 @@ impl Socks5Config {
|
||||
|
||||
Ok(socks_stream)
|
||||
}
|
||||
|
||||
fn to_url(&self) -> String {
|
||||
let host = utf8_percent_encode(&self.host, NON_ALPHANUMERIC_WITHOUT_DOT);
|
||||
if let Some((user, password)) = &self.user_password {
|
||||
let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
|
||||
let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
|
||||
format!("socks5://{user}:{password}@{host}:{}", self.port)
|
||||
} else {
|
||||
format!("socks5://{host}:{}", self.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -217,7 +246,7 @@ where
|
||||
|
||||
impl ProxyConfig {
|
||||
/// Creates a new proxy configuration by parsing given proxy URL.
|
||||
fn from_url(url: &str) -> Result<Self> {
|
||||
pub(crate) fn from_url(url: &str) -> Result<Self> {
|
||||
let url = Url::parse(url).context("Cannot parse proxy URL")?;
|
||||
match url.scheme() {
|
||||
"http" => {
|
||||
@@ -272,6 +301,19 @@ impl ProxyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes proxy config into an URL.
|
||||
///
|
||||
/// This function can be used to normalize proxy URL
|
||||
/// by parsing it and serializing back.
|
||||
pub(crate) fn to_url(&self) -> String {
|
||||
match self {
|
||||
Self::Http(http_config) => http_config.to_url("http"),
|
||||
Self::Https(http_config) => http_config.to_url("https"),
|
||||
Self::Socks5(socks5_config) => socks5_config.to_url(),
|
||||
Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
|
||||
/// config into `proxy_url` if `proxy_url` is unset or empty.
|
||||
///
|
||||
|
||||
+82
-45
@@ -23,7 +23,7 @@
|
||||
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
|
||||
//! and `joinRealtimeChannel().send()` just like the other peers.
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use email::Header;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
|
||||
@@ -143,9 +143,10 @@ impl Iroh {
|
||||
self.endpoint.add_node_addr(peer.clone())?;
|
||||
}
|
||||
|
||||
self.gossip
|
||||
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
|
||||
.await?;
|
||||
self.gossip.join_with_opts(
|
||||
topic,
|
||||
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -232,6 +233,7 @@ impl ChannelState {
|
||||
impl Context {
|
||||
/// Create iroh endpoint and gossip.
|
||||
async fn init_peer_channels(&self) -> Result<Iroh> {
|
||||
info!(self, "Initializing peer channels.");
|
||||
let secret_key = SecretKey::generate();
|
||||
let public_key = secret_key.public();
|
||||
|
||||
@@ -258,7 +260,14 @@ impl Context {
|
||||
|
||||
// create gossip
|
||||
let my_addr = endpoint.node_addr().await?;
|
||||
let gossip = Gossip::from_endpoint(endpoint.clone(), Default::default(), &my_addr.info);
|
||||
let gossip_config = iroh_gossip::proto::topic::Config {
|
||||
// Allow messages up to 128 KB in size.
|
||||
// We set the limit to 128 KiB to account for internal overhead,
|
||||
// but only guarantee 128 KB of payload to WebXDC developers.
|
||||
max_message_size: 128 * 1024,
|
||||
..Default::default()
|
||||
};
|
||||
let gossip = Gossip::from_endpoint(endpoint.clone(), gossip_config, &my_addr.info);
|
||||
|
||||
// spawn endpoint loop that forwards incoming connections to the gossiper
|
||||
let context = self.clone();
|
||||
@@ -277,6 +286,10 @@ impl Context {
|
||||
|
||||
/// Get or initialize the iroh peer channel.
|
||||
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");
|
||||
}
|
||||
|
||||
let ctx = self.clone();
|
||||
self.iroh
|
||||
.get_or_try_init(|| async { ctx.init_peer_channels().await })
|
||||
@@ -301,6 +314,47 @@ pub(crate) async fn iroh_add_peer_for_topic(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add gossip peer from `Iroh-Node-Addr` header to WebXDC message identified by `instance_id`.
|
||||
pub async fn add_gossip_peer_from_header(
|
||||
context: &Context,
|
||||
instance_id: MsgId,
|
||||
node_addr: &str,
|
||||
) -> Result<()> {
|
||||
if !context
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Adding iroh peer with address {node_addr:?} to the topic of {instance_id}."
|
||||
);
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: instance_id,
|
||||
});
|
||||
|
||||
let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
"Could not add iroh peer because {instance_id} has no topic."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
|
||||
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert topicId into the database so that we can use it to retrieve the topic.
|
||||
pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: TopicId) -> Result<()> {
|
||||
ctx.sql
|
||||
@@ -414,15 +468,15 @@ pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn create_random_topic() -> TopicId {
|
||||
/// Creates a new random gossip topic.
|
||||
fn create_random_topic() -> TopicId {
|
||||
TopicId::from_bytes(rand::random())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_iroh_header(
|
||||
ctx: &Context,
|
||||
topic: TopicId,
|
||||
msg_id: MsgId,
|
||||
) -> Result<Header> {
|
||||
/// Creates `Iroh-Gossip-Header` with a new random topic
|
||||
/// and stores the topic for the message.
|
||||
pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<Header> {
|
||||
let topic = create_random_topic();
|
||||
insert_topic_stub(ctx, msg_id, topic).await?;
|
||||
Ok(Header::new(
|
||||
HeaderDef::IrohGossipTopic.get_headername().to_string(),
|
||||
@@ -536,17 +590,6 @@ mod tests {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
bob.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
alice
|
||||
.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
@@ -576,6 +619,13 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
|
||||
assert!(msg_id == alice_webxdc.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
|
||||
|
||||
// Bob adds alice to gossip peers.
|
||||
@@ -678,17 +728,6 @@ mod tests {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
bob.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
alice
|
||||
.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await
|
||||
@@ -846,17 +885,6 @@ mod tests {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
bob.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
alice
|
||||
.ctx
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
@@ -925,6 +953,11 @@ mod tests {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
|
||||
.await
|
||||
@@ -942,6 +975,10 @@ mod tests {
|
||||
// creates iroh endpoint as side effect
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none())
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// This internal function should return error
|
||||
// if accidentally called with the setting disabled.
|
||||
assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+73
-22
@@ -343,7 +343,7 @@ impl Peerstate {
|
||||
}
|
||||
|
||||
/// Updates peerstate according to the given `Autocrypt` header.
|
||||
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
|
||||
pub fn apply_header(&mut self, context: &Context, header: &Aheader, message_time: i64) {
|
||||
if !addr_cmp(&self.addr, &header.addr) {
|
||||
return;
|
||||
}
|
||||
@@ -362,6 +362,13 @@ impl Peerstate {
|
||||
self.public_key = Some(header.public_key.clone());
|
||||
self.recalc_fingerprint();
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring outdated Autocrypt header because message_time={} < last_seen={}.",
|
||||
message_time,
|
||||
self.last_seen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,23 +773,65 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
|
||||
// If the from addr is different from the peerstate address we know,
|
||||
// we may want to do an AEAP transition.
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr)
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
&& mime_parser.has_chat_version()
|
||||
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
|
||||
// probably from a new contact sharing the same key.
|
||||
&& !mime_parser.signatures.is_empty()
|
||||
// Check if the From: address was also in the signed part of the email.
|
||||
// Without this check, an attacker could replay a message from Alice
|
||||
// to Bob. Then Bob's device would do an AEAP transition from Alice's
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
&& mime_parser.from_is_signed
|
||||
// DC avoids sending messages with the same timestamp, that's why `>` is here unlike in
|
||||
// `Peerstate::apply_header()`.
|
||||
&& info.message_time > peerstate.last_seen
|
||||
{
|
||||
if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) {
|
||||
// Check if it's a chat message; we do this to avoid
|
||||
// some accidental transitions if someone writes from multiple
|
||||
// addresses with an MUA.
|
||||
if !mime_parser.has_chat_version() {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because the message is not a chat message.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the message is encrypted and signed correctly. If it's not encrypted, it's
|
||||
// probably from a new contact sharing the same key.
|
||||
if mime_parser.signatures.is_empty() {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because the message is not encrypted and signed.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the From: address was also in the signed part of the email.
|
||||
// Without this check, an attacker could replay a message from Alice
|
||||
// to Bob. Then Bob's device would do an AEAP transition from Alice's
|
||||
// to the attacker's address, allowing for easier phishing.
|
||||
if !mime_parser.from_is_signed {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because From: is not signed.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// DC avoids sending messages with the same timestamp, that's why messages
|
||||
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
|
||||
if info.message_time <= peerstate.last_seen {
|
||||
info!(
|
||||
context,
|
||||
"Not doing AEAP from {} to {} because {} < {}.",
|
||||
&peerstate.addr,
|
||||
&mime_parser.from.addr,
|
||||
info.message_time,
|
||||
peerstate.last_seen
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
|
||||
);
|
||||
|
||||
let info = &mut mime_parser.decryption_info;
|
||||
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
|
||||
// Add info messages to chats with this (verified) contact
|
||||
@@ -800,7 +849,7 @@ pub(crate) async fn maybe_do_aeap_transition(
|
||||
let header = info.autocrypt_header.as_ref().context(
|
||||
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
|
||||
)?;
|
||||
peerstate.apply_header(header, info.message_time);
|
||||
peerstate.apply_header(context, header, info.message_time);
|
||||
|
||||
peerstate
|
||||
.save_to_db_ex(&context.sql, Some(&old_addr))
|
||||
@@ -979,6 +1028,8 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_peerstate_degrade_reordering() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
|
||||
let addr = "example@example.org";
|
||||
let pub_key = alice_keypair().public;
|
||||
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
|
||||
@@ -1003,7 +1054,7 @@ mod tests {
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
peerstate.apply_header(&header, 100);
|
||||
peerstate.apply_header(&ctx, &header, 100);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
peerstate.degrade_encryption(300);
|
||||
@@ -1011,11 +1062,11 @@ mod tests {
|
||||
|
||||
// This has message time 200, while encryption was degraded at timestamp 300.
|
||||
// Because of reordering, header should not be applied.
|
||||
peerstate.apply_header(&header, 200);
|
||||
peerstate.apply_header(&ctx, &header, 200);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
// Same header will be applied in the future.
|
||||
peerstate.apply_header(&header, 300);
|
||||
peerstate.apply_header(&ctx, &header, 300);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ enum SignedPublicKeyOrSubkey<'a> {
|
||||
Subkey(&'a SignedPublicSubKey),
|
||||
}
|
||||
|
||||
impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
impl KeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn fingerprint(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Key(k) => k.fingerprint(),
|
||||
@@ -64,7 +64,7 @@ impl<'a> KeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
|
||||
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn verify_signature(
|
||||
&self,
|
||||
hash: HashAlgorithm,
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ impl PushSubscriber {
|
||||
}
|
||||
|
||||
/// Sets device token for Apple Push Notification service.
|
||||
pub(crate) async fn set_device_token(&mut self, token: &str) {
|
||||
pub(crate) async fn set_device_token(&self, token: &str) {
|
||||
self.inner.write().await.device_token = Some(token.to_string());
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::net::http::post_empty;
|
||||
use crate::net::proxy::DEFAULT_SOCKS_PORT;
|
||||
use crate::net::proxy::{ProxyConfig, DEFAULT_SOCKS_PORT};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::token;
|
||||
use crate::tools::validate_id;
|
||||
@@ -446,7 +446,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
|
||||
let addr = ContactAddress::new(addr)?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
|
||||
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledSecurejoinQrScan)
|
||||
.await
|
||||
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
|
||||
|
||||
@@ -723,6 +723,10 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
.get_config(Config::ProxyUrl)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
// Normalize the URL.
|
||||
let url = ProxyConfig::from_url(&url)?.to_url();
|
||||
|
||||
let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
|
||||
.chain(
|
||||
old_proxy_url_value
|
||||
@@ -1266,7 +1270,8 @@ mod tests {
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_name(), "Jörn P. P.");
|
||||
assert_eq!(contact.get_authname(), "Jörn P. P.");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
@@ -1281,6 +1286,7 @@ mod tests {
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
@@ -1787,6 +1793,17 @@ mod tests {
|
||||
)
|
||||
);
|
||||
|
||||
// SOCKS5 config does not have port 1080 explicitly specified,
|
||||
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
|
||||
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,77 @@ use crate::qr::{self, Qr};
|
||||
use crate::securejoin;
|
||||
use crate::stock_str::{self, backup_transfer_qr};
|
||||
|
||||
/// Create a QR code from any input data.
|
||||
pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
|
||||
let all_size = 512.0;
|
||||
let qr_code_size = 416.0;
|
||||
let logo_size = 96.0;
|
||||
|
||||
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
|
||||
let mut svg = String::with_capacity(28000);
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
|
||||
d.attr("viewBox", format_args!("0 0 {all_size} {all_size}"))?;
|
||||
d.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?; // required for enabling xlink:href on browsers
|
||||
Ok(())
|
||||
})?
|
||||
.build(|w| {
|
||||
// background
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", 0)?;
|
||||
d.attr("y", 0)?;
|
||||
d.attr("width", all_size)?;
|
||||
d.attr("height", all_size)?;
|
||||
d.attr("style", "fill:#ffffff")?;
|
||||
Ok(())
|
||||
})?;
|
||||
// QR code
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(all_size - qr_code_size) / 2.0,
|
||||
((all_size - qr_code_size) / 2.0)
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
let scale = qr_code_size / qr.size() as f32;
|
||||
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
path_data += &format!("M{x},{y}h1v1h-1z");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")?;
|
||||
d.attr("d", path_data)?;
|
||||
d.attr("transform", format!("scale({scale})"))
|
||||
})
|
||||
})?;
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{}) scale(2)", // data in qr_overlay_delta.svg-part are 48 x 48, scaling by 2 results in desired logo_size of 96
|
||||
(all_size - logo_size) / 2.0,
|
||||
(all_size - logo_size) / 2.0
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw_escapable(include_str!("../assets/qr_overlay_delta.svg-part")))
|
||||
})?;
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
/// Returns SVG of the QR code to join the group or verify contact.
|
||||
///
|
||||
/// If `chat_id` is `None`, returns verification QR code.
|
||||
@@ -304,6 +375,14 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_qr_svg() -> Result<()> {
|
||||
let svg = create_qr_svg("this is a test QR code \" < > &")?;
|
||||
assert!(svg.contains("<svg"));
|
||||
assert!(svg.contains("</svg>"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_svg_escaping() {
|
||||
let svg = inner_generate_secure_join_qr_code(
|
||||
|
||||
+5
-4
@@ -11,7 +11,7 @@ use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::scan_folders::get_watched_folders;
|
||||
use crate::imap::session::Session as ImapSession;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::message::Message;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{stock_str, EventType};
|
||||
|
||||
@@ -25,6 +25,7 @@ pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
|
||||
|
||||
/// if quota is below this value (again),
|
||||
/// QuotaExceeding is cleared.
|
||||
///
|
||||
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
|
||||
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
|
||||
///
|
||||
@@ -73,7 +74,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||
) -> Result<(u64, &'t String, &'t QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
for r in resources {
|
||||
@@ -141,8 +142,8 @@ impl Context {
|
||||
Some(&highest.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::quota_exceeding(self, highest).await;
|
||||
let mut msg =
|
||||
Message::new_text(stock_str::quota_exceeding(self, highest).await);
|
||||
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
|
||||
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
|
||||
self.set_config_internal(Config::QuotaExceeding, None)
|
||||
|
||||
+56
-8
@@ -19,19 +19,20 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{send_msg, Chat, ChatId};
|
||||
use crate::chatlist_events;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
|
||||
use crate::message::{rfc724_mid_exists, Message, MsgId};
|
||||
use crate::param::Param;
|
||||
|
||||
/// A single reaction consisting of multiple emoji sequences.
|
||||
///
|
||||
/// It is guaranteed to have all emojis sorted and deduplicated inside.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct Reaction {
|
||||
/// Canonical representation of reaction as a string of space-separated emojis.
|
||||
reaction: String,
|
||||
@@ -173,7 +174,7 @@ async fn set_msg_id_reaction(
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
reaction: &Reaction,
|
||||
) -> Result<()> {
|
||||
if reaction.is_empty() {
|
||||
// Simply remove the record instead of setting it to empty string.
|
||||
@@ -228,8 +229,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
let reaction: Reaction = reaction.into();
|
||||
let mut reaction_msg = Message::new(Viewtype::Text);
|
||||
reaction_msg.text = reaction.as_str().to_string();
|
||||
let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
|
||||
reaction_msg.set_reaction();
|
||||
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
|
||||
reaction_msg.hidden = true;
|
||||
@@ -244,7 +244,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
msg.chat_id,
|
||||
ContactId::SELF,
|
||||
reaction_msg.timestamp_sort,
|
||||
reaction,
|
||||
&reaction,
|
||||
)
|
||||
.await?;
|
||||
Ok(reaction_msg_id)
|
||||
@@ -275,16 +275,28 @@ pub(crate) async fn set_msg_reaction(
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
is_incoming_fresh: bool,
|
||||
) -> Result<()> {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
||||
|
||||
if is_incoming_fresh
|
||||
&& !reaction.is_empty()
|
||||
&& msg_id.get_state(context).await?.is_outgoing()
|
||||
{
|
||||
context.emit_event(EventType::IncomingReaction {
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get our own reaction for a given message.
|
||||
@@ -563,6 +575,38 @@ Here's my footer -- bob@example.net"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expect_incoming_reactions_event(
|
||||
t: &TestContext,
|
||||
expected_msg_id: MsgId,
|
||||
expected_contact_id: ContactId,
|
||||
expected_reaction: &str,
|
||||
) -> Result<()> {
|
||||
let event = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingReaction {
|
||||
msg_id,
|
||||
contact_id,
|
||||
reaction,
|
||||
} => {
|
||||
assert_eq!(msg_id, expected_msg_id);
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
assert_eq!(reaction, Reaction::from(expected_reaction));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn has_incoming_reactions_event(t: &TestContext) -> bool {
|
||||
t.evtracker
|
||||
.get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. }))
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_reaction() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -593,6 +637,7 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
@@ -610,6 +655,7 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(bob_reaction.as_str(), "👍");
|
||||
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
|
||||
|
||||
// Alice reacts to own message.
|
||||
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
|
||||
@@ -650,6 +696,7 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg1.id, "👍").await?;
|
||||
let bob_send_reaction = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_send_reaction).await;
|
||||
assert!(has_incoming_reactions_event(&alice).await);
|
||||
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
@@ -665,6 +712,7 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?;
|
||||
let alice_send_reaction = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_opt(&alice_send_reaction).await;
|
||||
assert!(!has_incoming_reactions_event(&bob).await);
|
||||
|
||||
assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await;
|
||||
assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await;
|
||||
|
||||
+57
-33
@@ -30,7 +30,7 @@ use crate::message::{
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{get_iroh_topic_for_msg, insert_topic_stub, iroh_add_peer_for_topic};
|
||||
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::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
@@ -41,7 +41,6 @@ use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||
use crate::{chatlist_events, location};
|
||||
use crate::{contact, imap};
|
||||
use iroh_net::NodeAddr;
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
///
|
||||
@@ -762,6 +761,7 @@ async fn add_parts(
|
||||
let state: MessageState;
|
||||
let mut hidden = false;
|
||||
let mut needs_delete_job = false;
|
||||
let mut restore_protection = false;
|
||||
|
||||
// if contact renaming is prevented (for mailinglists and bots),
|
||||
// we use name from From:-header as override name
|
||||
@@ -930,15 +930,11 @@ async fn add_parts(
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to create a normal chat
|
||||
let create_blocked = if from_id == ContactId::SELF {
|
||||
Blocked::Not
|
||||
} else {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
match contact.is_blocked() {
|
||||
true => Blocked::Yes,
|
||||
false if is_bot => Blocked::Not,
|
||||
false => Blocked::Request,
|
||||
}
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
let create_blocked = match contact.is_blocked() {
|
||||
true => Blocked::Yes,
|
||||
false if is_bot => Blocked::Not,
|
||||
false => Blocked::Request,
|
||||
};
|
||||
|
||||
if let Some(chat) = test_normal_chat {
|
||||
@@ -1010,6 +1006,13 @@ async fn add_parts(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
|
||||
restore_protection = new_protection != ProtectionStatus::Protected
|
||||
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
|
||||
// Check that the contact still has the Autocrypt key same as the
|
||||
// verified key, see also `Peerstate::is_using_verified_key()`.
|
||||
&& contact.is_verified(context).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1032,8 +1035,7 @@ async fn add_parts(
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.first().copied().unwrap_or_default();
|
||||
|
||||
let self_sent =
|
||||
from_id == ContactId::SELF && to_ids.len() == 1 && to_ids.contains(&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);
|
||||
@@ -1091,8 +1093,8 @@ async fn add_parts(
|
||||
.await?;
|
||||
let now = tools::time();
|
||||
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::cant_decrypt_outgoing_msgs(context).await;
|
||||
let mut msg =
|
||||
Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.log_err(context)
|
||||
@@ -1255,11 +1257,13 @@ async fn add_parts(
|
||||
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let sort_to_bottom = false;
|
||||
let received = true;
|
||||
let sort_timestamp = chat_id
|
||||
.calc_sort_timestamp(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
sort_to_bottom,
|
||||
received,
|
||||
mime_parser.incoming,
|
||||
)
|
||||
.await?;
|
||||
@@ -1440,27 +1444,28 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
|
||||
match serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address") {
|
||||
Ok(node_addr) => {
|
||||
info!(context, "Adding iroh peer with address {node_addr:?}.");
|
||||
let instance_id = parent.context("Failed to get parent message")?.id;
|
||||
if let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? {
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server)
|
||||
.await?;
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
} else {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
|
||||
Some((instance_id, _ts_sent)) => {
|
||||
if let Err(err) =
|
||||
add_gossip_peer_from_header(context, instance_id, node_addr).await
|
||||
{
|
||||
warn!(context, "Failed to add iroh peer from header: {err:#}.");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Could not add iroh peer because {instance_id} has no topic"
|
||||
"Cannot add iroh peer because WebXDC instance does not exist."
|
||||
);
|
||||
}
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Couldn't parse NodeAddr: {err:#}.");
|
||||
},
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because the message has no In-Reply-To."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1468,6 +1473,7 @@ async fn add_parts(
|
||||
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;
|
||||
set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
@@ -1475,6 +1481,7 @@ async fn add_parts(
|
||||
from_id,
|
||||
sort_timestamp,
|
||||
Reaction::from(reaction_str.as_str()),
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1707,7 +1714,16 @@ RETURNING id
|
||||
// delete it.
|
||||
needs_delete_job = true;
|
||||
}
|
||||
|
||||
if restore_protection {
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
mime_parser.timestamp_rcvd,
|
||||
Some(from_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
@@ -1791,6 +1807,14 @@ async fn lookup_chat_by_reply(
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
|
||||
// If the parent chat is a 1:1 chat, then the message should go to the 1:1 chat.
|
||||
if to_ids.len() == 1 {
|
||||
let name = chat::get_other_chat_contacts(context, parent_chat_id).await?[0];
|
||||
if from_id != *name {
|
||||
mime_parser.set_override_sender_name(Some(name.get_stock_name(context).await));
|
||||
}
|
||||
return Ok(Some((parent_chat.id, parent_chat.blocked)));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
||||
@@ -883,6 +883,54 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_concat_multiple_ndns() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@posteo.org").await;
|
||||
let mid = "1234@mail.gmail.com";
|
||||
receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@posteo.org\n\
|
||||
To: hanerthaertidiuea@gmx.de\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <1234@mail.gmail.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
let msg_id = chats.get_msg_id(0)?.unwrap();
|
||||
|
||||
let raw = include_str!("../../test-data/message/posteo_ndn.eml");
|
||||
let raw = raw.replace(
|
||||
"Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>",
|
||||
&format!("Message-ID: <{}>", mid),
|
||||
);
|
||||
receive_imf(&t, raw.as_bytes(), false).await?;
|
||||
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
let err = "Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)".to_string();
|
||||
assert_eq!(msg.error(), Some(err.clone()));
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let raw = raw.replace(
|
||||
"Message-Id: <20200609184422.DCB6B1200DD@mout01.posteo.de>",
|
||||
"Message-Id: <next@mout01.posteo.de>",
|
||||
);
|
||||
receive_imf(&t, raw.as_bytes(), false).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
assert_eq!(msg.error(), Some([err.clone(), err].join("\n\n")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
@@ -2178,8 +2226,7 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::BccSelf, false).await?;
|
||||
let chat_id = bob.get_self_chat().await.id;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = "Happy birthday to me".to_string();
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
Ok(())
|
||||
@@ -3298,8 +3345,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
assert_eq!(received_group.name, "Group");
|
||||
assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request
|
||||
|
||||
let mut msg_out = Message::new(Viewtype::Text);
|
||||
msg_out.set_text("Private reply".to_string());
|
||||
let mut msg_out = Message::new_text("Private reply".to_string());
|
||||
|
||||
assert_eq!(received_group.blocked, Blocked::Request);
|
||||
msg_out.set_quote(&alice1, Some(&received)).await?;
|
||||
@@ -3506,8 +3552,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let received_group = Chat::load_from_db(&alice, received.chat_id).await?;
|
||||
assert_eq!(received_group.typ, Chattype::Group);
|
||||
|
||||
let mut msg_out = Message::new(Viewtype::Text);
|
||||
msg_out.set_text("Private reply".to_string());
|
||||
let mut msg_out = Message::new_text("Private reply".to_string());
|
||||
msg_out.set_quote(&alice, Some(&received)).await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
@@ -4811,6 +4856,36 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_older_message_from_2nd_device() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = alice
|
||||
.create_chat_with_contact("", "bob@example.net")
|
||||
.await
|
||||
.id;
|
||||
alice.send_text(chat_id, "We share this account").await;
|
||||
let received = receive_imf(
|
||||
alice,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <1234-2-4@example.org>\n\
|
||||
Date: Sat, 07 Dec 1970 19:00:26 +0000\n\
|
||||
\n\
|
||||
I'm Alice too\n",
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
alice
|
||||
.golden_test_chat(
|
||||
received.chat_id,
|
||||
"receive_imf_older_message_from_2nd_device",
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_create_adhoc_group_on_member_removal() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+2
-6
@@ -655,9 +655,7 @@ async fn fetch_idle(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
connection
|
||||
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
|
||||
.await?;
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
@@ -669,9 +667,7 @@ async fn fetch_idle(
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
connection
|
||||
.fake_idle(ctx, &mut session, watch_folder, folder_meaning)
|
||||
.await?;
|
||||
connection.fake_idle(ctx, watch_folder).await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
|
||||
+41
-45
@@ -1,13 +1,13 @@
|
||||
//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Error, Result};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
use anyhow::{ensure, Context as _, Error, Result};
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, get_chat_id_by_grpid, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::ensure_secret_key_exists;
|
||||
@@ -34,9 +34,6 @@ use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
||||
debug_assert!(
|
||||
progress <= 1000,
|
||||
@@ -89,12 +86,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
bail!("No fingerprint, cannot generate QR code.");
|
||||
}
|
||||
};
|
||||
let fingerprint = get_self_fingerprint(context).await?;
|
||||
|
||||
let self_addr_urlencoded =
|
||||
utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
@@ -140,14 +132,11 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
match load_self_public_key(context).await {
|
||||
Ok(key) => Some(key.fingerprint()),
|
||||
Err(_) => {
|
||||
warn!(context, "get_self_fingerprint(): failed to load key");
|
||||
None
|
||||
}
|
||||
}
|
||||
async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
|
||||
let key = load_self_public_key(context)
|
||||
.await
|
||||
.context("Failed to load key")?;
|
||||
Ok(key.fingerprint())
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
@@ -388,7 +377,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
let fingerprint: Fingerprint = fp.parse()?;
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
if !encrypted_and_signed(context, mime_message, &fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring {step} message because the message is not encrypted."
|
||||
@@ -577,11 +566,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
if !encrypted_and_signed(
|
||||
context,
|
||||
mime_message,
|
||||
get_self_fingerprint(context).await.as_ref(),
|
||||
) {
|
||||
if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -740,24 +725,19 @@ async fn mark_peer_as_verified(
|
||||
fn encrypted_and_signed(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
expected_fingerprint: Option<&Fingerprint>,
|
||||
expected_fingerprint: &Fingerprint,
|
||||
) -> bool {
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if let Some(expected_fingerprint) = expected_fingerprint {
|
||||
if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Fingerprint for comparison missing.");
|
||||
} else if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,6 +765,7 @@ mod tests {
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
AliceHasName,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -812,10 +793,21 @@ mod tests {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_has_name() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
@@ -860,7 +852,10 @@ mod tests {
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
@@ -994,6 +989,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
@@ -1023,14 +1019,13 @@ mod tests {
|
||||
}
|
||||
|
||||
// Make sure Alice hasn't yet sent their name to Bob.
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "");
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
};
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
@@ -1053,6 +1048,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
|
||||
@@ -73,8 +73,9 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// Calculate the sort timestamp before checking the chat protection status so that if we
|
||||
// race with its change, we don't add our message below the protection message.
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts_sort = chat_id
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, false)
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
|
||||
.await?;
|
||||
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
||||
let ts_start = time();
|
||||
|
||||
@@ -270,7 +270,7 @@ impl BobState {
|
||||
context,
|
||||
"Bob Step 4 - handling {{vc,vg}}-auth-required message."
|
||||
);
|
||||
if !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) {
|
||||
if !encrypted_and_signed(context, mime_message, self.invite.fingerprint()) {
|
||||
let reason = if mime_message.was_encrypted() {
|
||||
"Valid signature missing"
|
||||
} else {
|
||||
|
||||
+7
-3
@@ -126,6 +126,7 @@ impl Smtp {
|
||||
|
||||
let login_params =
|
||||
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
|
||||
let mut first_error = None;
|
||||
for lp in login_params {
|
||||
info!(context, "SMTP trying to connect to {}.", &lp.connection);
|
||||
let transport = match connect::connect_and_auth(
|
||||
@@ -143,6 +144,7 @@ impl Smtp {
|
||||
Ok(transport) => transport,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP failed to connect and authenticate: {err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -157,7 +159,7 @@ impl Smtp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format_err!("SMTP failed to connect"))
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No SMTP connection candidates provided")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,8 +372,10 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
)
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.").await?;
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")
|
||||
.await?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
|
||||
@@ -168,6 +168,7 @@ async fn connect_stream(
|
||||
connect_insecure_proxy(context, host, port, proxy_config.clone()).await?
|
||||
}
|
||||
};
|
||||
update_connection_history(context, "smtp", host, port, host, time()).await?;
|
||||
Ok(stream)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
|
||||
+145
-100
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use rusqlite::{config::DbConfig, types::ValueRef, Connection, OpenFlags, Row};
|
||||
use tokio::sync::{Mutex, MutexGuard, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||
@@ -17,7 +17,7 @@ use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::prune_connection_history;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -60,11 +60,6 @@ pub struct Sql {
|
||||
/// Database file path
|
||||
pub(crate) dbfile: PathBuf,
|
||||
|
||||
/// Write transactions mutex.
|
||||
///
|
||||
/// See [`Self::write_lock`].
|
||||
write_mtx: Mutex<()>,
|
||||
|
||||
/// SQL connection pool.
|
||||
pool: RwLock<Option<Pool>>,
|
||||
|
||||
@@ -81,7 +76,6 @@ impl Sql {
|
||||
pub fn new(dbfile: PathBuf) -> Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
write_mtx: Mutex::new(()),
|
||||
pool: Default::default(),
|
||||
is_encrypted: Default::default(),
|
||||
config_cache: Default::default(),
|
||||
@@ -147,7 +141,8 @@ impl Sql {
|
||||
let mut config_cache = self.config_cache.write().await;
|
||||
config_cache.clear();
|
||||
|
||||
self.call_write(move |conn| {
|
||||
let query_only = false;
|
||||
self.call(query_only, move |conn| {
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
@@ -159,29 +154,28 @@ impl Sql {
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
// Without resetting import may fail due to existing tables.
|
||||
let res = res.and_then(|_| {
|
||||
res.and_then(|_| {
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
|
||||
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
|
||||
});
|
||||
let res = res.and_then(|_| {
|
||||
})
|
||||
.and_then(|_| {
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")
|
||||
});
|
||||
let res = res.and(
|
||||
})
|
||||
.and(
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
|
||||
);
|
||||
let res = res.and_then(|_| {
|
||||
)
|
||||
.and_then(|_| {
|
||||
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
})
|
||||
.context("failed to import from attached backup database")
|
||||
});
|
||||
let res = res.and(
|
||||
})
|
||||
.and(
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database"),
|
||||
);
|
||||
res?;
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -252,8 +246,7 @@ impl Sql {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(stock_str::delete_server_turned_off(context).await);
|
||||
let mut msg = Message::new_text(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config_internal(Config::DeleteServerAfter, Some("0"))
|
||||
@@ -325,7 +318,8 @@ impl Sql {
|
||||
let mut lock = self.pool.write().await;
|
||||
|
||||
let pool = lock.take().context("SQL connection pool is not open")?;
|
||||
let conn = pool.get().await?;
|
||||
let query_only = false;
|
||||
let conn = pool.get(query_only).await?;
|
||||
if !passphrase.is_empty() {
|
||||
conn.pragma_update(None, "rekey", passphrase.clone())
|
||||
.context("Failed to set PRAGMA rekey")?;
|
||||
@@ -337,59 +331,20 @@ impl Sql {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
/// multiple write transactions at once.
|
||||
/// Allocates a connection and calls `function` with the connection.
|
||||
///
|
||||
/// Doing the locking ourselves instead of relying on SQLite has these reasons:
|
||||
///
|
||||
/// - SQLite's locking mechanism is non-async, blocking a thread
|
||||
/// - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// More considerations on alternatives to the current approach:
|
||||
///
|
||||
/// We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
|
||||
///
|
||||
/// In order to never get concurrency issues, we could make all transactions IMMEDIATE,
|
||||
/// but this would mean that there can never be two simultaneous transactions.
|
||||
///
|
||||
/// Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
|
||||
///
|
||||
/// DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
|
||||
///
|
||||
/// 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
|
||||
/// 2. If there are other write transactions, we block the db connection until
|
||||
/// upgraded. If some reader comes then, it has to get the next, less used connection with a
|
||||
/// worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
|
||||
/// 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
|
||||
/// 4. If upon a successful upgrade to a write transaction the db has been modified,
|
||||
/// the transaction has to be rolled back and retried, which means extra work in terms of
|
||||
/// CPU/battery.
|
||||
///
|
||||
/// The only pro of making write transactions DEFERRED w/o the external locking would be some
|
||||
/// parallelism between them.
|
||||
///
|
||||
/// Another option would be to make write transactions IMMEDIATE, also
|
||||
/// w/o the external locking. But then cons 1. - 3. above would still be valid.
|
||||
pub async fn write_lock(&self) -> MutexGuard<'_, ()> {
|
||||
self.write_mtx.lock().await
|
||||
}
|
||||
|
||||
/// Allocates a connection and calls `function` with the connection. If `function` does write
|
||||
/// queries,
|
||||
/// - either first take a lock using `write_lock()`
|
||||
/// - or use `call_write()` instead.
|
||||
/// If `query_only` is true, allocates read-only connection,
|
||||
/// otherwise allocates write connection.
|
||||
///
|
||||
/// Returns the result of the function.
|
||||
async fn call<'a, F, R>(&'a self, function: F) -> Result<R>
|
||||
async fn call<'a, F, R>(&'a self, query_only: bool, function: F) -> Result<R>
|
||||
where
|
||||
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let lock = self.pool.read().await;
|
||||
let pool = lock.as_ref().context("no SQL connection")?;
|
||||
let mut conn = pool.get().await?;
|
||||
let mut conn = pool.get(query_only).await?;
|
||||
let res = tokio::task::block_in_place(move || function(&mut conn))?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -403,8 +358,8 @@ impl Sql {
|
||||
F: 'a + FnOnce(&mut Connection) -> Result<R> + Send,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let _lock = self.write_lock().await;
|
||||
self.call(function).await
|
||||
let query_only = false;
|
||||
self.call(query_only, function).await
|
||||
}
|
||||
|
||||
/// Execute `query` assuming it is a write query, returning the number of affected rows.
|
||||
@@ -444,7 +399,8 @@ impl Sql {
|
||||
G: Send + FnMut(rusqlite::MappedRows<F>) -> Result<H>,
|
||||
H: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let res = stmt.query_map(params, f)?;
|
||||
g(res)
|
||||
@@ -476,7 +432,8 @@ impl Sql {
|
||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T> + Send,
|
||||
T: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let res = conn.query_row(query, params, f)?;
|
||||
Ok(res)
|
||||
})
|
||||
@@ -512,7 +469,8 @@ impl Sql {
|
||||
|
||||
/// Query the database if the requested table already exists.
|
||||
pub async fn table_exists(&self, name: &str) -> Result<bool> {
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut exists = false;
|
||||
conn.pragma(None, "table_info", name.to_string(), |_row| {
|
||||
// will only be executed if the info was found
|
||||
@@ -527,7 +485,8 @@ impl Sql {
|
||||
|
||||
/// Check if a column exists in a given table.
|
||||
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
|
||||
self.call(move |conn| {
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
let mut exists = false;
|
||||
// `PRAGMA table_info` returns one row per column,
|
||||
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
|
||||
@@ -555,10 +514,13 @@ impl Sql {
|
||||
F: Send + FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||
T: Send + 'static,
|
||||
{
|
||||
self.call(move |conn| match conn.query_row(sql.as_ref(), params, f) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
let query_only = true;
|
||||
self.call(query_only, move |conn| {
|
||||
match conn.query_row(sql.as_ref(), params, f) {
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -717,6 +679,36 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
// Tries to clear the freelist to free some space on the disk.
|
||||
//
|
||||
// This only works if auto_vacuum is enabled.
|
||||
async fn incremental_vacuum(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.call_write(move |conn| {
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA incremental_vacuum")
|
||||
.context("Failed to prepare incremental_vacuum statement")?;
|
||||
|
||||
// It is important to step the statement until it returns no more rows.
|
||||
// Otherwise it will not free as many pages as it can:
|
||||
// <https://stackoverflow.com/questions/53746807/sqlite-incremental-vacuum-removing-only-one-free-page>.
|
||||
let mut rows = stmt
|
||||
.query(())
|
||||
.context("Failed to run incremental_vacuum statement")?;
|
||||
let mut row_count = 0;
|
||||
while let Some(_row) = rows
|
||||
.next()
|
||||
.context("Failed to step incremental_vacuum statement")?
|
||||
{
|
||||
row_count += 1;
|
||||
}
|
||||
info!(context, "Incremental vacuum freed {row_count} pages.");
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cleanup the account to restore some storage and optimize the database.
|
||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
// Setting `Config::LastHousekeeping` at the beginning avoids endless loops when things do not
|
||||
@@ -749,24 +741,8 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
// Try to clear the freelist to free some space on the disk. This
|
||||
// only works if auto_vacuum is enabled.
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional("PRAGMA incremental_vacuum", (), |_row| Ok(()))
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
Ok(Some(())) => {
|
||||
// Incremental vacuum returns a zero-column result if it did anything.
|
||||
info!(context, "Successfully ran incremental vacuum.");
|
||||
}
|
||||
Ok(None) => {
|
||||
// Incremental vacuum returned `SQLITE_DONE` immediately,
|
||||
// there were no pages to remove.
|
||||
}
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
|
||||
context
|
||||
@@ -1092,9 +1068,10 @@ mod tests {
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let query_only = true;
|
||||
let auto_vacuum = t
|
||||
.sql
|
||||
.call(|conn| {
|
||||
.call(query_only, |conn| {
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
@@ -1150,8 +1127,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let mut new_draft = Message::new(Viewtype::Text);
|
||||
new_draft.set_text("This is my draft".to_string());
|
||||
let mut new_draft = Message::new_text("This is my draft".to_string());
|
||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||
|
||||
housekeeping(&t).await.unwrap();
|
||||
@@ -1320,8 +1296,9 @@ mod tests {
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let conn1 = pool.get().await?;
|
||||
let conn2 = pool.get().await?;
|
||||
let query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
@@ -1346,4 +1323,72 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// `query_row` does not acquire write lock
|
||||
// and operates on read-only connection.
|
||||
// Using it to `INSERT` should fail.
|
||||
let res = t
|
||||
.sql
|
||||
.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
||||
("xyz", "ijk"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// If you want to `INSERT` and get value via `RETURNING`,
|
||||
// use `call_write` or `transaction`.
|
||||
|
||||
let res: Result<u32> = t
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let val = conn.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
||||
("foo", "bar"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 2);
|
||||
|
||||
let res = t
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
let val = t.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
||||
("abc", "def"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that incremental_vacuum does not fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_incremental_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
incremental_vacuum(&t).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,6 +1046,30 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 123)?;
|
||||
if dbversion < migration_version {
|
||||
// Add FOREIGN KEY(msg_id).
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE new_msgs_status_updates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
uid TEXT UNIQUE,
|
||||
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT OR IGNORE INTO new_msgs_status_updates SELECT
|
||||
id, msg_id, update_item, uid
|
||||
FROM msgs_status_updates;
|
||||
DROP TABLE msgs_status_updates;
|
||||
ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
|
||||
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
+106
-17
@@ -7,23 +7,67 @@
|
||||
//! Each SQLite connection has its own page cache, so allocating recently used connections
|
||||
//! improves the performance compared to, for example, organizing the pool as a queue
|
||||
//! and returning the least recently used connection each time.
|
||||
//!
|
||||
//! Pool returns at most one write connection (with `PRAGMA query_only=0`).
|
||||
//! This ensures that there never are multiple write transactions at once.
|
||||
//!
|
||||
//! Doing the locking ourselves instead of relying on SQLite has these reasons:
|
||||
//!
|
||||
//! - SQLite's locking mechanism is non-async, blocking a thread
|
||||
//! - SQLite's locking mechanism just sleeps in a loop, which is really inefficient
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! More considerations on alternatives to the current approach:
|
||||
//!
|
||||
//! We use [DEFERRED](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions) transactions.
|
||||
//!
|
||||
//! In order to never get concurrency issues, we could make all transactions IMMEDIATE,
|
||||
//! but this would mean that there can never be two simultaneous transactions.
|
||||
//!
|
||||
//! Read transactions can simply be made DEFERRED to run in parallel w/o any drawbacks.
|
||||
//!
|
||||
//! DEFERRED write transactions without doing the locking ourselves would have these drawbacks:
|
||||
//!
|
||||
//! 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
|
||||
//! 2. If there are other write transactions, we block the db connection until
|
||||
//! upgraded. If some reader comes then, it has to get the next, less used connection with a
|
||||
//! worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
|
||||
//! 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
|
||||
//! 4. If upon a successful upgrade to a write transaction the db has been modified,
|
||||
//! the transaction has to be rolled back and retried, which means extra work in terms of
|
||||
//! CPU/battery.
|
||||
//!
|
||||
//! The only pro of making write transactions DEFERRED w/o the external locking would be some
|
||||
//! parallelism between them.
|
||||
//!
|
||||
//! Another option would be to make write transactions IMMEDIATE, also
|
||||
//! w/o the external locking. But then cons 1. - 3. above would still be valid.
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore};
|
||||
|
||||
/// Inner connection pool.
|
||||
#[derive(Debug)]
|
||||
struct InnerPool {
|
||||
/// Available connections.
|
||||
connections: Mutex<Vec<Connection>>,
|
||||
connections: parking_lot::Mutex<Vec<Connection>>,
|
||||
|
||||
/// Counts the number of available connections.
|
||||
semaphore: Arc<Semaphore>,
|
||||
|
||||
/// Write mutex.
|
||||
///
|
||||
/// This mutex ensures there is at most
|
||||
/// one write connection with `query_only=0`.
|
||||
///
|
||||
/// This mutex is locked when write connection
|
||||
/// is outside the pool.
|
||||
write_mutex: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl InnerPool {
|
||||
@@ -35,6 +79,56 @@ impl InnerPool {
|
||||
connections.push(connection);
|
||||
drop(connections);
|
||||
}
|
||||
|
||||
/// Retrieves a connection from the pool.
|
||||
///
|
||||
/// Sets `query_only` pragma to the provided value
|
||||
/// 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.
|
||||
pub async fn get(self: Arc<Self>, query_only: bool) -> Result<PooledConnection> {
|
||||
if query_only {
|
||||
let permit = self.semaphore.clone().acquire_owned().await?;
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock();
|
||||
connections
|
||||
.pop()
|
||||
.context("Got a permit when there are no connections in the pool")?
|
||||
};
|
||||
conn.pragma_update(None, "query_only", "1")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
_write_mutex_guard: None,
|
||||
};
|
||||
Ok(conn)
|
||||
} else {
|
||||
// We get write guard first to avoid taking a permit
|
||||
// and not using it, blocking a reader from getting a connection
|
||||
// while being ourselves blocked by another wrtier.
|
||||
let write_mutex_guard = Arc::clone(&self.write_mutex).lock_owned().await;
|
||||
|
||||
// We may still have to wait for a connection
|
||||
// to be returned by some reader.
|
||||
let permit = self.semaphore.clone().acquire_owned().await?;
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock();
|
||||
connections.pop().context(
|
||||
"Got a permit and write lock when there are no connections in the pool",
|
||||
)?
|
||||
};
|
||||
conn.pragma_update(None, "query_only", "0")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
_write_mutex_guard: Some(write_mutex_guard),
|
||||
};
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pooled connection.
|
||||
@@ -47,6 +141,11 @@ pub struct PooledConnection {
|
||||
|
||||
/// Semaphore permit, dropped after returning the connection to the pool.
|
||||
_permit: OwnedSemaphorePermit,
|
||||
|
||||
/// Write mutex guard.
|
||||
///
|
||||
/// `None` for read-only connections with `PRAGMA query_only=1`.
|
||||
_write_mutex_guard: Option<OwnedMutexGuard<()>>,
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
@@ -86,24 +185,14 @@ impl Pool {
|
||||
pub fn new(connections: Vec<Connection>) -> Self {
|
||||
let semaphore = Arc::new(Semaphore::new(connections.len()));
|
||||
let inner = Arc::new(InnerPool {
|
||||
connections: Mutex::new(connections),
|
||||
connections: parking_lot::Mutex::new(connections),
|
||||
semaphore,
|
||||
write_mutex: Default::default(),
|
||||
});
|
||||
Pool { inner }
|
||||
}
|
||||
|
||||
/// Retrieves a connection from the pool.
|
||||
pub async fn get(&self) -> Result<PooledConnection> {
|
||||
let permit = self.inner.semaphore.clone().acquire_owned().await?;
|
||||
let mut connections = self.inner.connections.lock();
|
||||
let conn = connections
|
||||
.pop()
|
||||
.context("got a permit when there are no connections in the pool")?;
|
||||
let conn = PooledConnection {
|
||||
pool: Arc::downgrade(&self.inner),
|
||||
conn: Some(conn),
|
||||
_permit: permit,
|
||||
};
|
||||
Ok(conn)
|
||||
pub async fn get(&self, query_only: bool) -> Result<PooledConnection> {
|
||||
Arc::clone(&self.inner).get(query_only).await
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -542,7 +542,7 @@ impl ContactId {
|
||||
}
|
||||
|
||||
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
|
||||
async fn get_stock_name(self, context: &Context) -> String {
|
||||
pub async fn get_stock_name(self, context: &Context) -> String {
|
||||
Contact::get_by_id(context, self)
|
||||
.await
|
||||
.map(|contact| contact.get_display_name().to_string())
|
||||
@@ -1419,8 +1419,7 @@ impl Context {
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = welcome_message(self).await;
|
||||
let mut msg = Message::new_text(welcome_message(self).await);
|
||||
chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-4
@@ -305,8 +305,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.to_string());
|
||||
let msg = Message::new_text(some_text.to_string());
|
||||
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
@@ -415,8 +414,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Forwarded
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(some_text.clone());
|
||||
let mut msg = Message::new_text(some_text.clone());
|
||||
msg.param.set_int(Param::Forwarded, 1);
|
||||
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
|
||||
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
|
||||
|
||||
+19
-2
@@ -730,8 +730,7 @@ impl TestContext {
|
||||
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
|
||||
/// the message.
|
||||
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage<'_> {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = txt.to_string();
|
||||
let mut msg = Message::new_text(txt.to_string());
|
||||
self.send_msg(chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
@@ -1106,6 +1105,24 @@ impl EventTracker {
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes emitted events returning the first matching one if any.
|
||||
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
event_matcher: F,
|
||||
) -> Option<EventType> {
|
||||
ctx.emit_event(EventType::Test);
|
||||
loop {
|
||||
let event = self.recv().await.unwrap();
|
||||
if event_matcher(&event.typ) {
|
||||
return Some(event.typ);
|
||||
}
|
||||
if let EventType::Test = event.typ {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes events looking for an [`EventType::Info`] with substring matching.
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
self.get_matching(|evt| match evt {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::message::Message;
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -297,6 +297,7 @@ async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
|
||||
assert!(chat.is_protected());
|
||||
|
||||
for alice_accepts_breakage in [true, false] {
|
||||
SystemTime::shift(std::time::Duration::from_secs(300));
|
||||
// Bob uses Thunderbird to send a message
|
||||
receive_imf(
|
||||
&alice,
|
||||
@@ -715,8 +716,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
assert!(!alice_bob_chat.can_send(&alice).await?);
|
||||
|
||||
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("Draftttt".to_string());
|
||||
let mut msg = Message::new_text("Draftttt".to_string());
|
||||
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
|
||||
assert_eq!(
|
||||
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
|
||||
@@ -730,6 +730,50 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob_old = &tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob_old]).await;
|
||||
mark_as_verified(bob_old, alice).await;
|
||||
bob_old.configure_addr("bob@example.net").await;
|
||||
let chat = bob_old.create_chat(alice).await;
|
||||
let sent_old = bob_old
|
||||
.send_text(chat.id, "Soon i'll have a new device")
|
||||
.await;
|
||||
SystemTime::shift(std::time::Duration::from_secs(3600));
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[bob]).await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
|
||||
tcm.send_recv(bob, alice, "Now i have it!").await;
|
||||
assert_verified(alice, bob, ProtectionStatus::Protected).await;
|
||||
|
||||
let msg = alice.recv_msg(&sent_old).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let contact = alice.add_or_lookup_contact(bob).await;
|
||||
// The outdated Bob's Autocrypt header isn't applied, so the verification preserves.
|
||||
assert!(contact.is_verified(alice).await.unwrap());
|
||||
let chat = alice.get_chat(bob).await;
|
||||
assert!(chat.is_protected());
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
let protection_msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
protection_msg.param.get_cmd(),
|
||||
SystemMessage::ChatProtectionEnabled
|
||||
);
|
||||
assert!(protection_msg.timestamp_sort >= msg.timestamp_rcvd);
|
||||
alice
|
||||
.golden_test_chat(msg.chat_id, "verified_chats_message_from_old_dc_setup")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the following bug:
|
||||
///
|
||||
/// - Scan your chat partner's QR Code
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@
|
||||
use std::cmp::{max, min};
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
|
||||
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
||||
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 30;
|
||||
|
||||
/// Smeared timestamp generator.
|
||||
#[derive(Debug)]
|
||||
@@ -157,11 +157,11 @@ mod tests {
|
||||
// because there were not enough timestamps in the past.
|
||||
assert_eq!(smeared_timestamp.current(), now + 4);
|
||||
|
||||
// Forward another 7 messages.
|
||||
// We cannot use more than 5 timestamps from the future,
|
||||
// so we use 5 timestamps from the future,
|
||||
// Forward another 32 messages.
|
||||
// We cannot use more than 30 timestamps from the future,
|
||||
// so we use 30 timestamps from the future,
|
||||
// the current timestamp and one timestamp from the past.
|
||||
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 1);
|
||||
assert_eq!(smeared_timestamp.create_n(now, 32), now - 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+1
-2
@@ -250,8 +250,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
|
||||
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
|
||||
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = stock_str::update_reminder_msg_body(context).await;
|
||||
let mut msg = Message::new_text(stock_str::update_reminder_msg_body(context).await);
|
||||
if let Some(timestamp) = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0) {
|
||||
add_device_msg(
|
||||
context,
|
||||
|
||||
@@ -36,7 +36,7 @@ use crate::{chat, location};
|
||||
use std::collections::{hash_map, HashMap};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::message::{Message, MsgId};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::color::color_int_to_hex_string;
|
||||
@@ -85,8 +85,7 @@ pub(crate) async fn intercept_send_update(
|
||||
ChatId::create_for_contact(context, ContactId::SELF).await?
|
||||
};
|
||||
|
||||
let mut poi_msg = Message::new(Viewtype::Text);
|
||||
poi_msg.text = label;
|
||||
let mut poi_msg = Message::new_text(label);
|
||||
poi_msg.set_location(lat, lng);
|
||||
chat::send_msg(context, chat_id, &mut poi_msg).await?;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: Me (Contact#Contact#Self): We share this account √
|
||||
Msg#11: Me (Contact#Contact#Self): I'm Alice too √
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,7 +1,8 @@
|
||||
Single#Chat#10: Bob [bob@example.net]
|
||||
Single#Chat#10: Bob [bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from my verified device! [FRESH]
|
||||
Msg#12: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
Msg#13: (Contact#Contact#10): Old, unverified message [SEEN]
|
||||
Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Now i have it! [FRESH]
|
||||
Msg#12: info (Contact#Contact#Info): bob@example.net sent a message from another device. [NOTICED][INFO 🛡️❌]
|
||||
Msg#13: (Contact#Contact#10): Soon i'll have a new device [FRESH]
|
||||
Msg#14: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
--------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user