mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
72 Commits
v1.139.1
...
link2xt/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9dbf05d8d | ||
|
|
90c30879b1 | ||
|
|
0ca1318118 | ||
|
|
0be639b244 | ||
|
|
48b4cfc247 | ||
|
|
a4037b8278 | ||
|
|
30405056e3 | ||
|
|
0fbab7147a | ||
|
|
de57ef5ac7 | ||
|
|
f48a047fe0 | ||
|
|
8ba08432c5 | ||
|
|
bf34bd3a62 | ||
|
|
21845ca5ea | ||
|
|
768ef772bb | ||
|
|
69842c18f7 | ||
|
|
42a7cd3eea | ||
|
|
b7e5b906d1 | ||
|
|
ad271fac80 | ||
|
|
70ad323c9a | ||
|
|
27bf4c37a7 | ||
|
|
1cc31c1038 | ||
|
|
adb0dd43a7 | ||
|
|
d29538beb0 | ||
|
|
b99e4649a4 | ||
|
|
68daa3550e | ||
|
|
9d65282710 | ||
|
|
d8f3368b3c | ||
|
|
5755fe7bef | ||
|
|
4f071e3b31 | ||
|
|
f4dfc79808 | ||
|
|
518d5bc4c7 | ||
|
|
0e1f62a38d | ||
|
|
af4b59fe0a | ||
|
|
8c3c0484ed | ||
|
|
97828234dd | ||
|
|
20e64c71f8 | ||
|
|
2214d140c3 | ||
|
|
907d3efcd0 | ||
|
|
9573e02c32 | ||
|
|
8cb699290a | ||
|
|
31d7b4f9ce | ||
|
|
2e5ad3f3a0 | ||
|
|
5d3d5d23a1 | ||
|
|
469ff799ad | ||
|
|
18f2a09b35 | ||
|
|
81f6aec1a0 | ||
|
|
ff60605a7f | ||
|
|
7010e80336 | ||
|
|
5f790c1dbc | ||
|
|
8c5d8477fb | ||
|
|
10fe6929b0 | ||
|
|
6fc0000c8a | ||
|
|
e84a5589df | ||
|
|
e7d9ff12ec | ||
|
|
607f5959ab | ||
|
|
11546a1ce9 | ||
|
|
ee671836ca | ||
|
|
dd77d32446 | ||
|
|
b32fb05ab8 | ||
|
|
918d87dcb6 | ||
|
|
98ae05ee59 | ||
|
|
cff5c064a6 | ||
|
|
e9cef4b0ba | ||
|
|
7f2c8ff53d | ||
|
|
46d6b81058 | ||
|
|
6d59fb49aa | ||
|
|
97602f3fd7 | ||
|
|
f17987743e | ||
|
|
5767cce178 | ||
|
|
20a4bb1a88 | ||
|
|
578f29f215 | ||
|
|
6c9643e39e |
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -125,9 +125,6 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
@@ -212,11 +209,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
# Currently used Python version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -246,6 +243,7 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -266,11 +264,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: windows-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -292,6 +290,7 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -50,4 +50,4 @@ result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
.direnv
|
||||
|
||||
156
CHANGELOG.md
156
CHANGELOG.md
@@ -1,5 +1,155 @@
|
||||
# Changelog
|
||||
|
||||
## [1.140.0] - 2024-06-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
|
||||
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
|
||||
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
|
||||
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
|
||||
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
|
||||
- Allow creation of groups by outgoing messages without recipients.
|
||||
- Prefer `Chat-Group-ID` over references for new groups.
|
||||
- Do not fail to send images with wrong extensions.
|
||||
|
||||
### Build system
|
||||
|
||||
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Remove cargo-nextest bug workaround.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add vCard as supported standard.
|
||||
- Create_group() does not find chats, only creates them.
|
||||
- Fix a typo in test_partial_group_consistency().
|
||||
|
||||
### Refactor
|
||||
|
||||
- Factor create_adhoc_group() call out of create_group().
|
||||
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix logging of TestContext created using TestContext::new_alice().
|
||||
- Refactor `test_alias_*` into 8 separate tests.
|
||||
|
||||
## [1.139.6] - 2024-05-25
|
||||
|
||||
### Build system
|
||||
|
||||
- Update `iroh` to the git version.
|
||||
- nix: Add iroh-base output hash.
|
||||
- Upgrade iroh to 0.17.0.
|
||||
|
||||
### Fixes
|
||||
|
||||
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
|
||||
- Acquire write lock on iroh_channels before checking for subscribe_loop.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Fix python lint.
|
||||
- cargo-deny: Remove unused entry from deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Log IMAP connection type on connection failure.
|
||||
|
||||
### Tests
|
||||
|
||||
- Viewtype::File attachments are sent unchanged and preserve extensions.
|
||||
- deltachat-rpc-client: Add realtime channel tests.
|
||||
- deltachat-rpc-client: Regression test for double gossip subscription.
|
||||
|
||||
## [1.139.5] - 2024-05-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-ffi: Make WebXdcRealtimeData data usable in CFFI.
|
||||
- Add event channel overflow event.
|
||||
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_DATA constant.
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_advertisement().
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_data().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-repl: Add start-realtime and send-realtime commands.
|
||||
|
||||
### Fixes
|
||||
|
||||
- peer_channels: Connect to peers that advertise to you.
|
||||
- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)).
|
||||
|
||||
### Tests
|
||||
|
||||
- peer_channels: Add test_parallel_connect().
|
||||
- "SecureJoin wait" state and info messages.
|
||||
|
||||
## [1.139.4] - 2024-05-21
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Scale up contact origins to OutgoingTo when sending a message.
|
||||
- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not log warning if iroh relay metadata is NIL.
|
||||
- contact-tools: Parse_vcard: Support `\r\n` newlines.
|
||||
- Make_vcard: Add authname and key for ContactId::SELF.
|
||||
|
||||
### Other
|
||||
|
||||
- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)).
|
||||
|
||||
## [1.139.3] - 2024-05-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] @deltachat/stdio-rpc-server: change api: don't search in path unless `options.takeVersionFromPATH` is set to `true`
|
||||
- @deltachat/stdio-rpc-server: remove `DELTA_CHAT_SKIP_PATH` environment variable
|
||||
- @deltachat/stdio-rpc-server: remove version check / search for dc rpc server in $PATH
|
||||
- @deltachat/stdio-rpc-server: remove `options.skipSearchInPath`
|
||||
- @deltachat/stdio-rpc-server: add `options.takeVersionFromPATH`
|
||||
- deltachat-rpc-client: Add Account.wait_for_incoming_msg().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Replace env_logger with tracing_subscriber.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore event channel overflows.
|
||||
- mimeparser: Take the last header of multiple ones with the same name.
|
||||
- Db migration version 59, it contained an sql syntax error.
|
||||
- Sql syntax error in db migration 27.
|
||||
- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)).
|
||||
- @deltachat/stdio-rpc-server: set default options for `startDeltaChat`.
|
||||
- Always convert absolute paths to relative in accounts.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- receive_imf: Do not check for ContactId::UNDEFINED.
|
||||
- receive_imf: Remove unnecessary check for is_mdn.
|
||||
- receive_imf: Only call create_or_lookup_group() with allow_creation=true.
|
||||
- Use let..else in create_or_lookup_group().
|
||||
- Stop trying to extract chat ID from Message-IDs.
|
||||
- Do not try to lookup group in create_or_lookup_group().
|
||||
|
||||
## [1.139.2] - 2024-05-18
|
||||
|
||||
### Build system
|
||||
|
||||
- Add repository URL to @deltachat/jsonrpc-client.
|
||||
|
||||
## [1.139.1] - 2024-05-18
|
||||
|
||||
### CI
|
||||
@@ -4215,3 +4365,9 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5
|
||||
[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0
|
||||
[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1
|
||||
[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2
|
||||
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
|
||||
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
|
||||
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
|
||||
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
|
||||
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0
|
||||
|
||||
453
Cargo.lock
generated
453
Cargo.lock
generated
@@ -512,23 +512,6 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bao-tree"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f7a89a8ee5889d2593ae422ce6e1bb03e48a0e8a16e4fa0882dfcbe7e182ef"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-lite 2.3.0",
|
||||
"genawaiter",
|
||||
"iroh-blake3",
|
||||
"iroh-io",
|
||||
"positioned-io",
|
||||
"range-collections",
|
||||
"self_cell",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.1.1"
|
||||
@@ -577,12 +560,6 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "binary-merge"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@@ -1347,7 +1324,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1390,12 +1367,10 @@ dependencies = [
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"openssl-src",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
"pretty_assertions",
|
||||
"pretty_env_logger",
|
||||
"proptest",
|
||||
"qrcodegen",
|
||||
"quick-xml",
|
||||
@@ -1404,7 +1379,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ratelimit",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest 0.11.27",
|
||||
"rusqlite",
|
||||
"rust-hsluv",
|
||||
"sanitize-filename",
|
||||
@@ -1443,7 +1418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.2.1",
|
||||
@@ -1451,7 +1426,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"deltachat",
|
||||
"deltachat-contact-tools",
|
||||
"env_logger 0.11.3",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"log",
|
||||
"num-traits",
|
||||
@@ -1468,33 +1443,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"dirs",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"rusqlite",
|
||||
"rustyline",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
"deltachat-jsonrpc",
|
||||
"env_logger 0.11.3",
|
||||
"futures-lite 2.3.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing-subscriber",
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
@@ -1512,7 +1487,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1586,7 +1561,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ef71ddb5b3a1f53dee24817c8f70dfa1cb29e804c18d88c228d4bc9c86ee3b9"
|
||||
dependencies = [
|
||||
"proc-macro-error 1.0.4",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -2150,19 +2125,6 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.3"
|
||||
@@ -2586,8 +2548,6 @@ checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"genawaiter-macro",
|
||||
"genawaiter-proc-macro",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2596,19 +2556,6 @@ version = "0.99.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
|
||||
|
||||
[[package]]
|
||||
name = "genawaiter-proc-macro"
|
||||
version = "0.99.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
|
||||
dependencies = [
|
||||
"proc-macro-error 0.4.12",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -3029,9 +2976,26 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.28",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.2.0",
|
||||
"hyper-util",
|
||||
"rustls 0.22.4",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3054,6 +3018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
@@ -3061,6 +3026,9 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3195,15 +3163,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inplace-vec-builder"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@@ -3222,7 +3181,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-sys 0.48.0",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3257,8 +3216,8 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"rcgen 0.10.0",
|
||||
"ring 0.16.20",
|
||||
"rustls",
|
||||
"rustls-webpki",
|
||||
"rustls 0.21.11",
|
||||
"rustls-webpki 0.101.7",
|
||||
"serde",
|
||||
"serde-error",
|
||||
"ssh-key 0.5.1",
|
||||
@@ -3277,18 +3236,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-base"
|
||||
version = "0.16.2"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02a1a5323b25a181b1434a44f9f59ebc478d21156cf9bf91aa850ad0d626f833"
|
||||
checksum = "b1be0b442ed44d20905cf77c673169906c883e05c829e3fb303b131e925139fc"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"anyhow",
|
||||
"bao-tree",
|
||||
"crypto_box",
|
||||
"data-encoding",
|
||||
"derive_more 1.0.0-beta.6",
|
||||
"ed25519-dalek 2.1.1",
|
||||
"getrandom 0.2.12",
|
||||
"hex",
|
||||
"iroh-blake3",
|
||||
"once_cell",
|
||||
"postcard",
|
||||
"rand 0.8.5",
|
||||
@@ -3317,9 +3277,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-gossip"
|
||||
version = "0.16.2"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11daf47e11d00016eeac662b8b13343d40233764fe4b971e0d6cf15b1c98f0a1"
|
||||
checksum = "857aa77a0b29283edf99224bd12cd739684c5873e639b85adb38b8d1d777162d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3341,22 +3301,11 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iroh-io"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d1047ad5ca29ab4ff316b6830d86e7ea52cea54325e4d4a849692e1274b498"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-lite 2.3.0",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iroh-metrics"
|
||||
version = "0.16.2"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b4f668653628979461eabe56853a694b1eb4713e87ed25f2224618165c0e67"
|
||||
checksum = "dd54b9cf342b2618efc8d3ff6cdcd083fa5a2cf6cc78bb473bd32e228eabb40e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"erased_set",
|
||||
@@ -3365,7 +3314,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"once_cell",
|
||||
"prometheus-client",
|
||||
"reqwest",
|
||||
"reqwest 0.12.4",
|
||||
"serde",
|
||||
"struct_iterable",
|
||||
"time 0.3.34",
|
||||
@@ -3375,13 +3324,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-net"
|
||||
version = "0.16.2"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bea6e221dfbe6301965a5ec9b6bae2c156375a4baafdbdbad7a93c3dcf950d6"
|
||||
checksum = "4a744000e6c5704479eeb4751eb23b9b8e7e56a0fc484beb8831694fc93e378f"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"anyhow",
|
||||
"axum",
|
||||
"backoff",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"der 0.7.8",
|
||||
"derive_more 1.0.0-beta.6",
|
||||
@@ -3420,12 +3371,12 @@ dependencies = [
|
||||
"postcard",
|
||||
"rand 0.8.5",
|
||||
"rand_core 0.6.4",
|
||||
"rcgen 0.11.3",
|
||||
"reqwest",
|
||||
"rcgen 0.12.1",
|
||||
"reqwest 0.12.4",
|
||||
"ring 0.17.8",
|
||||
"rtnetlink",
|
||||
"rustls",
|
||||
"rustls-webpki",
|
||||
"rustls 0.21.11",
|
||||
"rustls-webpki 0.101.7",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
@@ -3435,13 +3386,13 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time 0.3.34",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls-acme",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"watchable",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.25.4",
|
||||
"windows 0.51.1",
|
||||
"wmi",
|
||||
"x509-parser 0.15.1",
|
||||
@@ -3450,16 +3401,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn"
|
||||
version = "0.10.4"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b934380145fd5d53a583d01ae9500f4807efe6b0f0fe115c7be4afa2b35db99f"
|
||||
checksum = "906875956feb75d3d41d708ddaffeb11fdb10cd05f23efbcb17600037e411779"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"iroh-quinn-proto",
|
||||
"iroh-quinn-udp",
|
||||
"pin-project-lite",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3467,15 +3418,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn-proto"
|
||||
version = "0.10.7"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f2656b322c7f6cf3eb95e632d1c0f2fa546841915b0270da581f918c70c4be"
|
||||
checksum = "c6bf92478805e67f2320459285496e1137edf5171411001a0d4d85f9bbafb792"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring 0.16.20",
|
||||
"ring 0.17.8",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"rustls-native-certs",
|
||||
"slab",
|
||||
"thiserror",
|
||||
@@ -3485,9 +3436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn-udp"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6679979a7271c24f9dae9622c0b4a543881508aa3a7396f55dfbaaa56f01c063"
|
||||
checksum = "edc7915b3a31f08ee0bc02f73f4d61a5d5be146a1081ef7f70622a11627fd314"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -4141,9 +4092,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.63"
|
||||
version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"cfg-if",
|
||||
@@ -4173,18 +4124,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.1.6+3.1.4"
|
||||
version = "300.3.0+3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
|
||||
checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.99"
|
||||
version = "0.9.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
|
||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -4506,7 +4457,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"ed25519-dalek 2.1.1",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"reqwest 0.11.27",
|
||||
"self_cell",
|
||||
"simple-dns",
|
||||
"thiserror",
|
||||
@@ -4669,16 +4620,6 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||
|
||||
[[package]]
|
||||
name = "positioned-io"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccabfeeb89c73adf4081f0dca7f8e28dbda90981a222ceea37f619e93ea6afe9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.0.8"
|
||||
@@ -4759,16 +4700,6 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_env_logger"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
|
||||
dependencies = [
|
||||
"env_logger 0.10.2",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
@@ -4787,45 +4718,19 @@ dependencies = [
|
||||
"toml_edit 0.21.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr 0.4.12",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr 1.0.4",
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn-mid",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
@@ -4837,12 +4742,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.20+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
@@ -4957,7 +4856,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4973,7 +4872,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ring 0.16.20",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"rustls-native-certs",
|
||||
"slab",
|
||||
"thiserror",
|
||||
@@ -5115,18 +5014,6 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-collections"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca9edd21e2db51000ac63eccddabba622f826e631a60be7bade9bd6a76b69537"
|
||||
dependencies = [
|
||||
"binary-merge",
|
||||
"inplace-vec-builder",
|
||||
"ref-cast",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratelimit"
|
||||
version = "1.0.0"
|
||||
@@ -5172,18 +5059,6 @@ dependencies = [
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6"
|
||||
dependencies = [
|
||||
"pem 3.0.4",
|
||||
"ring 0.16.20",
|
||||
"time 0.3.34",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.12.1"
|
||||
@@ -5225,26 +5100,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
@@ -5310,7 +5165,7 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
"hyper-rustls",
|
||||
"hyper-rustls 0.24.2",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
@@ -5320,8 +5175,8 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls 0.21.11",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -5329,14 +5184,55 @@ dependencies = [
|
||||
"system-configuration 0.5.1",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body-util",
|
||||
"hyper 1.2.0",
|
||||
"hyper-rustls 0.26.0",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.22.4",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"tokio",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.1",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5540,10 +5436,24 @@ checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.17.8",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.17.8",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.3"
|
||||
@@ -5551,7 +5461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
@@ -5565,6 +5475,22 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -5575,6 +5501,17 @@ dependencies = [
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
|
||||
dependencies = [
|
||||
"ring 0.17.8",
|
||||
"rustls-pki-types",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
@@ -6255,17 +6192,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn-mid"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
@@ -6381,15 +6307,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testdir"
|
||||
version = "0.9.1"
|
||||
@@ -6568,7 +6485,18 @@ version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
|
||||
dependencies = [
|
||||
"rustls 0.22.4",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -6587,16 +6515,16 @@ dependencies = [
|
||||
"pem 3.0.4",
|
||||
"proc-macro2",
|
||||
"rcgen 0.12.1",
|
||||
"reqwest",
|
||||
"reqwest 0.11.27",
|
||||
"ring 0.17.8",
|
||||
"rustls",
|
||||
"rustls 0.21.11",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.25.4",
|
||||
"x509-parser 0.16.0",
|
||||
]
|
||||
|
||||
@@ -6879,7 +6807,7 @@ checksum = "2835fe6badda3e20a012d19d6593ded0fc11f659d5d5152394061ffbb03b4b04"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"ident_case",
|
||||
"proc-macro-error 1.0.4",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -7168,6 +7096,15 @@ version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
@@ -7523,6 +7460,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wmi"
|
||||
version = "0.13.3"
|
||||
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -61,8 +61,8 @@ hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
|
||||
iroh-net = "0.16.2"
|
||||
iroh-gossip = { version = "0.16.2", features = ["net"] }
|
||||
iroh-net = "0.17.0"
|
||||
iroh-gossip = { version = "0.17.0", features = ["net"] }
|
||||
quinn = "0.10.0"
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
@@ -76,7 +76,6 @@ once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
@@ -105,21 +104,12 @@ toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
# Pin OpenSSL to 3.1 releases.
|
||||
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||
# 3.1 branch will be supported until 2025-03-14.
|
||||
openssl-src = "~300.1"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.9.0"
|
||||
@@ -181,4 +171,4 @@ vendored = [
|
||||
"async-native-tls/vendored",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
]
|
||||
]
|
||||
|
||||
@@ -155,7 +155,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\n[\t ]").unwrap());
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
@@ -643,10 +643,10 @@ END:VCARD
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_android_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
fn test_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
||||
// This one is Android-like.
|
||||
let vcard0 = "BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
@@ -656,13 +656,16 @@ PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
||||
|
||||
END:VCARD
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
";
|
||||
// This one is DOS-like.
|
||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
||||
for vcard in [vcard0, vcard1.as_str()] {
|
||||
let contacts = parse_vcard(vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -523,6 +523,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 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.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -6284,6 +6287,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
/**
|
||||
* Data received over an ephemeral peer channel.
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 (int) + (char*) binary data.
|
||||
* length is returned as integer with dc_event_get_data2_int()
|
||||
* and binary data is returned as dc_event_get_data2_str().
|
||||
* Binary data must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
@@ -6312,6 +6327,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
|
||||
/**
|
||||
* Inform that some events have been skipped due to event channel overflow.
|
||||
*
|
||||
* @param data1 (int) number of events that have been skipped
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -566,6 +566,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,6 +625,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
EventType::EventChannelOverflow { n } => *n as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,13 +660,13 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::WebxdcRealtimeData { .. }
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
@@ -679,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
status_update_serial,
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,12 +728,12 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::WebxdcRealtimeData { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -746,6 +749,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::WebxdcRealtimeData { data, .. } => {
|
||||
let ptr = libc::malloc(data.len());
|
||||
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -1447,7 +1447,7 @@ impl CommandApi {
|
||||
|
||||
/// Parses a vCard file located at the given path. Returns contacts in their original order.
|
||||
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||
let vcard = fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat_contact_tools::parse_vcard(vcard)
|
||||
.into_iter()
|
||||
@@ -1455,6 +1455,20 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Imports contacts from a vCard file located at the given path.
|
||||
///
|
||||
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat::contact::import_vcard(&ctx, vcard)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns a vCard containing contacts with the given ids.
|
||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -263,6 +263,9 @@ pub enum EventType {
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow { n: u64 },
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -378,6 +381,7 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
@@ -54,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.139.1"
|
||||
"version": "1.140.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -11,10 +11,10 @@ anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.31"
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -19,6 +19,7 @@ use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::reaction::send_reaction;
|
||||
@@ -642,6 +643,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("{cnt} chats");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
bail!("missing msgid");
|
||||
}
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
println!("waiting for peer channel join");
|
||||
res.await?;
|
||||
}
|
||||
println!("joined peer channel");
|
||||
}
|
||||
"send-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
bail!("missing msgid");
|
||||
}
|
||||
if arg2.is_empty() {
|
||||
bail!("no message");
|
||||
}
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
|
||||
println!("sent realtime message");
|
||||
}
|
||||
"chat" => {
|
||||
if sel_chat.is_none() && arg1.is_empty() {
|
||||
bail!("Argument [chat-id] is missing.");
|
||||
|
||||
@@ -32,6 +32,7 @@ use rustyline::{
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
@@ -483,9 +484,10 @@ async fn handle_cmd(
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
pretty_env_logger::formatted_timed_builder()
|
||||
.parse_default_env()
|
||||
.filter_module("deltachat_repl", log::LevelFilter::Info)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = std::env::args().collect();
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -17,6 +17,8 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -297,6 +297,12 @@ class Account:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event."""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
|
||||
@@ -62,6 +62,7 @@ class EventType(str, Enum):
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
@@ -70,3 +70,11 @@ class Message:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
@@ -126,8 +126,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
@@ -135,13 +134,15 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
message = alice.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
snapshot = message.get_snapshot()
|
||||
chat_id = snapshot.chat_id
|
||||
alice._rpc.download_full_message(alice.id, message.id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
@@ -177,8 +178,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
@@ -189,8 +189,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
|
||||
186
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
186
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Testing webxdc iroh connectivity
|
||||
|
||||
If you want to debug iroh at rust-trace/log level set
|
||||
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def path_to_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
|
||||
assert p.exists()
|
||||
return str(p)
|
||||
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "play"
|
||||
|
||||
# send iroh announcements simultaneously
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
return ac1_webxdc_msg, ac2_webxdc_msg
|
||||
|
||||
|
||||
def setup_thread_send_realtime_data(msg, data):
|
||||
def thread_run():
|
||||
for _i in range(10):
|
||||
msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
|
||||
def wait_receive_realtime_data(msg_data_list):
|
||||
account = msg_data_list[0][0].account
|
||||
msg_data_list = msg_data_list[:]
|
||||
|
||||
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
|
||||
while msg_data_list:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
for i, (msg, data) in enumerate(msg_data_list):
|
||||
if msg.id == event.msg_id:
|
||||
assert data == event.data
|
||||
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||
del msg_data_list[i]
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
# send iroh announcements sequentially
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
|
||||
|
||||
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")
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
|
||||
|
||||
|
||||
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
|
||||
|
||||
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
|
||||
|
||||
|
||||
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
"""Test that messages are received only once."""
|
||||
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()
|
||||
ac2_webxdc_msg.get_snapshot().chat.accept()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
|
||||
|
||||
# Issue a "send" call in parallel with sending advertisement.
|
||||
# Previously due to a bug this caused subscribing to the channel twice.
|
||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
def thread_run():
|
||||
for i in range(10):
|
||||
data = str(i).encode()
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
n = int(bytes(event.data).decode())
|
||||
break
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
break
|
||||
@@ -28,5 +28,5 @@ commands =
|
||||
|
||||
[pytest]
|
||||
timeout = 300
|
||||
log_cli = true
|
||||
#log_cli = true
|
||||
log_level = debug
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -14,13 +14,13 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -46,12 +46,11 @@ 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. in PATH
|
||||
- unless `DELTA_CHAT_SKIP_PATH=1` is specified
|
||||
- searches in .cargo/bin directory first
|
||||
- but there an additional version check is performed
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
3. prebuilds in npm packages
|
||||
|
||||
so by default it uses the prebuilds.
|
||||
|
||||
## How do you built this package in CI
|
||||
|
||||
- To build platform packages, run the `build_platform_package.py` script:
|
||||
|
||||
4
deltachat-rpc-server/npm-package/index.d.ts
vendored
4
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
export interface SearchOptions {
|
||||
/** whether to disable looking for deltachat-rpc-server inside of $PATH */
|
||||
skipSearchInPath: boolean;
|
||||
/** whether take deltachat-rpc-server inside of $PATH*/
|
||||
takeVersionFromPATH: boolean;
|
||||
|
||||
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
|
||||
disableEnvPath: boolean;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
//@ts-check
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { stat, readdir } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join, basename } from "node:path";
|
||||
import process from "node:process";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
ENV_VAR_NAME,
|
||||
PATH_EXECUTABLE_NAME,
|
||||
SKIP_SEARCH_IN_PATH,
|
||||
} from "./src/const.js";
|
||||
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||
import {
|
||||
ENV_VAR_LOCATION_NOT_FOUND,
|
||||
FAILED_TO_START_SERVER_EXECUTABLE,
|
||||
@@ -39,38 +33,13 @@ function findRPCServerInNodeModules() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getLocationInPath() {
|
||||
const exec = promisify(execFile);
|
||||
|
||||
if (os.platform() === "win32") {
|
||||
const { stdout: executable } = await exec("where", [PATH_EXECUTABLE_NAME], {
|
||||
shell: true,
|
||||
});
|
||||
return executable;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: executable } = await exec(
|
||||
"command",
|
||||
["-v", PATH_EXECUTABLE_NAME],
|
||||
{ shell: true }
|
||||
);
|
||||
return executable;
|
||||
} catch (error) {
|
||||
if (error.code > 0) return "";
|
||||
else throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||
export async function getRPCServerPath(
|
||||
options = { skipSearchInPath: false, disableEnvPath: false }
|
||||
) {
|
||||
// @TODO: improve confusing naming of these options
|
||||
const { skipSearchInPath, disableEnvPath } = options;
|
||||
export async function getRPCServerPath(options = {}) {
|
||||
const { takeVersionFromPATH, disableEnvPath } = {
|
||||
takeVersionFromPATH: false,
|
||||
disableEnvPath: false,
|
||||
...options,
|
||||
};
|
||||
// 1. check if it is set as env var
|
||||
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||
try {
|
||||
@@ -85,35 +54,9 @@ export async function getRPCServerPath(
|
||||
return process.env[ENV_VAR_NAME];
|
||||
}
|
||||
|
||||
// 2. check if it can be found in PATH
|
||||
if (!process.env[SKIP_SEARCH_IN_PATH] && !skipSearchInPath) {
|
||||
const executable = await getLocationInPath();
|
||||
|
||||
// by just trying to execute it and then use "command -v deltachat-rpc-server" (unix) or "where deltachat-rpc-server" (windows) to get the path to the executable
|
||||
if (executable.length > 1) {
|
||||
// test if it is the right version
|
||||
try {
|
||||
// for some unknown reason it is in stderr and not in stdout
|
||||
const { stderr } = await promisify(execFile)(
|
||||
executable,
|
||||
["--version"],
|
||||
{ shell: true }
|
||||
);
|
||||
const version = stderr.slice(0, stderr.indexOf("\n"));
|
||||
if (package_json.version !== version) {
|
||||
throw new Error(
|
||||
`version mismatch: (npm package: ${package_json.version}) (installed ${PATH_EXECUTABLE_NAME} version: ${version})`
|
||||
);
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Found executable in PATH, but there was an error: " + error
|
||||
);
|
||||
console.error("So falling back to using prebuild...");
|
||||
}
|
||||
}
|
||||
// 2. check if PATH should be used
|
||||
if (takeVersionFromPATH) {
|
||||
return PATH_EXECUTABLE_NAME;
|
||||
}
|
||||
// 3. check for prebuilds
|
||||
|
||||
@@ -123,11 +66,11 @@ export async function getRPCServerPath(
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||
export async function startDeltaChat(directory, options) {
|
||||
export async function startDeltaChat(directory, options = {}) {
|
||||
const pathToServerBinary = await getRPCServerPath(options);
|
||||
const server = spawn(pathToServerBinary, {
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG || "info",
|
||||
RUST_LOG: process.env.RUST_LOG,
|
||||
DC_ACCOUNTS_PATH: directory,
|
||||
},
|
||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.139.1"
|
||||
"version": "1.140.0"
|
||||
}
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
|
||||
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
export const SKIP_SEARCH_IN_PATH = "DELTA_CHAT_SKIP_PATH"
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
@@ -11,6 +11,7 @@ use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use yerpc::RpcServer as _;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
@@ -28,6 +29,9 @@ async fn main() {
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
if let Err(error) = &r {
|
||||
log::error!("Fatal error: {error:#}.")
|
||||
}
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
@@ -60,7 +64,13 @@ async fn main_impl() -> Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interferring with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
|
||||
11
deny.toml
11
deny.toml
@@ -45,7 +45,6 @@ skip = [
|
||||
{ name = "dlopen2", version = "0.4.1" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ name = "fastrand", version = "1.9.0" },
|
||||
@@ -53,6 +52,7 @@ skip = [
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "http-body", version = "0.4.6" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hyper-rustls", version = "0.24.2" },
|
||||
{ name = "hyper", version = "0.14.28" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "netlink-packet-core", version = "0.5.0" },
|
||||
@@ -62,8 +62,6 @@ skip = [
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pem", version = "1.1.1" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "proc-macro-error-attr", version = "0.4.12" },
|
||||
{ name = "proc-macro-error", version = "0.4.12" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -72,7 +70,11 @@ skip = [
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "rustls-pemfile", version = "1.0.4" },
|
||||
{ name = "rustls", version = "0.21.11" },
|
||||
{ name = "rustls-webpki", version = "0.101.7" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
@@ -86,9 +88,11 @@ skip = [
|
||||
{ name = "system-configuration-sys", version = "0.5.0" },
|
||||
{ name = "system-configuration", version = "0.5.1" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-rustls", version = "0.24.1" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "webpki-roots", version ="0.25.4" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
{ name = "windows-core", version = "<0.54.0" },
|
||||
@@ -102,6 +106,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
{ name = "x509-parser", version = "<0.16.0" },
|
||||
]
|
||||
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW: 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
@@ -66,6 +67,7 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
|
||||
@@ -37,7 +37,9 @@ module.exports = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum C {
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW = 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
@@ -66,6 +67,7 @@ export enum C {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
@@ -338,7 +340,9 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.139.1"
|
||||
"version": "1.140.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.139.1"
|
||||
version = "1.140.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-05-18
|
||||
2024-06-04
|
||||
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
@@ -485,10 +485,6 @@ impl Config {
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
|
||||
let dir = file
|
||||
.parent()
|
||||
.context("Cannot get config file directory")?
|
||||
.to_path_buf();
|
||||
let mut config = Self::new_nosync(file, writable).await?;
|
||||
let bytes = fs::read(&config.file)
|
||||
.await
|
||||
@@ -500,9 +496,13 @@ impl Config {
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
if account.dir.is_absolute() {
|
||||
if let Some(old_path_parent) = account.dir.parent() {
|
||||
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
76
src/blob.rs
76
src/blob.rs
@@ -3,7 +3,7 @@
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -12,6 +12,7 @@ use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::io::Reader as ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -426,9 +427,25 @@ impl<'a> BlobObject<'a> {
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
let mut file = std::fs::File::open(self.to_abs_path())?;
|
||||
let (nr_bytes, exif) = image_metadata(&file)?;
|
||||
*no_exif_ref = exif.is_none();
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
// It's strange that BufReader modifies a file position while it takes a non-mut
|
||||
// reference. Ok, just rewind it.
|
||||
file.rewind()?;
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
|
||||
let imgreader = match imgreader {
|
||||
Ok(ir) => ir,
|
||||
_ => {
|
||||
file.rewind()?;
|
||||
ImageReader::with_format(
|
||||
std::io::BufReader::new(&file),
|
||||
ImageFormat::from_path(&blob_abs)?,
|
||||
)
|
||||
}
|
||||
};
|
||||
let fmt = imgreader.format().context("No format??")?;
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
@@ -457,10 +474,9 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
let fmt = ImageFormat::from_path(&blob_abs);
|
||||
let ofmt = match fmt {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
ImageFormat::Jpeg => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
@@ -497,7 +513,7 @@ impl<'a> BlobObject<'a> {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
|
||||
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
@@ -538,7 +554,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
if !matches!(fmt, ImageFormat::Jpeg)
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
@@ -575,15 +591,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns image file size and Exif.
|
||||
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let file = std::fs::File::open(self.to_abs_path())?;
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(&file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
/// Returns image file size and Exif.
|
||||
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
@@ -1191,6 +1206,21 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::File,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Sticker,
|
||||
@@ -1291,8 +1321,7 @@ mod tests {
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
@@ -1321,9 +1350,13 @@ mod tests {
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
if viewtype == Viewtype::File {
|
||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||
let bytes1 = fs::read(&file_saved).await?;
|
||||
assert_eq!(&bytes1, bytes);
|
||||
}
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
@@ -1359,8 +1392,7 @@ mod tests {
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (file_size, _) = blob.metadata()?;
|
||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
|
||||
43
src/chat.rs
43
src/chat.rs
@@ -292,7 +292,7 @@ impl ChatId {
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
|
||||
.await
|
||||
.map(|chat| chat.id)?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
|
||||
chat_id
|
||||
} else {
|
||||
warn!(
|
||||
@@ -489,7 +489,7 @@ impl ChatId {
|
||||
// went to "contact requests" list rather than normal chatlist.
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != ContactId::SELF {
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -1932,6 +1932,10 @@ impl Chat {
|
||||
// reset encrypt error state eg. for forwarding
|
||||
msg.param.remove(Param::ErroneousE2ee);
|
||||
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
msg.param
|
||||
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
|
||||
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
@@ -2619,6 +2623,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let send_as_is = msg.viewtype == Viewtype::File;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
@@ -2645,8 +2650,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
}
|
||||
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
|
||||
if !send_as_is
|
||||
&& (msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
|
||||
{
|
||||
blob.recode_to_image_size(context, &mut maybe_sticker)
|
||||
.await?;
|
||||
@@ -2989,11 +2995,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.update_subject(context).await?;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
@@ -7526,4 +7528,27 @@ mod tests {
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending JPEG image with .png extension.
|
||||
///
|
||||
/// This is a regression test, previously sending failed
|
||||
/// because image was passed to PNG decoder
|
||||
/// and it failed to decode image.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_jpeg_with_png_ext() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
let file = alice.get_blobdir().join("screenshot.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let _msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,8 +363,8 @@ pub enum Config {
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
|
||||
/// Iroh secret key.
|
||||
IrohSecretKey,
|
||||
/// Enable webxdc realtime features.
|
||||
WebxdcRealtimeEnabled,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
304
src/contact.rs
304
src/contact.rs
@@ -1,6 +1,6 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::cmp::{min, Reverse};
|
||||
use std::collections::BinaryHeap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -11,8 +11,8 @@ use async_channel::{self as channel, Receiver, Sender};
|
||||
use base64::Engine as _;
|
||||
pub use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{
|
||||
self as contact_tools, addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr,
|
||||
strip_rtlo_characters, ContactAddress, VcardContact,
|
||||
self as contact_tools, addr_cmp, addr_normalize, sanitize_name_and_addr, strip_rtlo_characters,
|
||||
ContactAddress, VcardContact,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rusqlite::OptionalExtension;
|
||||
@@ -20,14 +20,15 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MessageState;
|
||||
@@ -36,7 +37,9 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, smeared_time, time, SystemTime,
|
||||
};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -120,6 +123,29 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the origin of the contacts, but only if `origin` is higher than the current one.
|
||||
pub(crate) async fn scaleup_origin(
|
||||
context: &Context,
|
||||
ids: &[Self],
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
|
||||
sql::repeat_vars(ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[origin])
|
||||
.chain(params_iter(ids))
|
||||
.chain(params_iter(&[origin])),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -166,9 +192,13 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
let mut vcard_contacts = Vec::with_capacity(contacts.len());
|
||||
for id in contacts {
|
||||
let c = Contact::get_by_id(context, *id).await?;
|
||||
let key = Peerstate::from_addr(context, &c.addr)
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.peek_key(false).map(|k| k.to_base64()));
|
||||
let key = match *id {
|
||||
ContactId::SELF => Some(load_self_public_key(context).await?),
|
||||
_ => Peerstate::from_addr(context, &c.addr)
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.take_key(false)),
|
||||
};
|
||||
let key = key.map(|k| k.to_base64());
|
||||
let profile_image = match c.get_profile_image(context).await? {
|
||||
None => None,
|
||||
Some(path) => tokio::fs::read(path)
|
||||
@@ -189,6 +219,129 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
Ok(contact_tools::make_vcard(&vcard_contacts))
|
||||
}
|
||||
|
||||
/// Imports contacts from the given vCard.
|
||||
///
|
||||
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
|
||||
/// regardless of whether they are just created, modified or left untouched.
|
||||
pub async fn import_vcard(context: &Context, vcard: &str) -> Result<Vec<ContactId>> {
|
||||
let contacts = contact_tools::parse_vcard(vcard);
|
||||
let mut contact_ids = Vec::with_capacity(contacts.len());
|
||||
for c in &contacts {
|
||||
let Ok(id) = import_vcard_contact(context, c)
|
||||
.await
|
||||
.with_context(|| format!("import_vcard_contact() failed for {}", c.addr))
|
||||
.log_err(context)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
contact_ids.push(id);
|
||||
}
|
||||
Ok(contact_ids)
|
||||
}
|
||||
|
||||
async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Result<ContactId> {
|
||||
let addr = ContactAddress::new(&contact.addr).context("Invalid address")?;
|
||||
// Importing a vCard is also an explicit user action like creating a chat with the contact. We
|
||||
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
|
||||
// want `contact.authname` to be saved as the authname and not a locally given name.
|
||||
let origin = Origin::CreateChat;
|
||||
let (id, modified) =
|
||||
match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
|
||||
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
|
||||
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
|
||||
Ok(val) => val,
|
||||
};
|
||||
if modified != Modifier::None {
|
||||
context.emit_event(EventType::ContactsChanged(Some(id)));
|
||||
}
|
||||
let key = contact.key.as_ref().and_then(|k| {
|
||||
SignedPublicKey::from_base64(k)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"import_vcard_contact: Cannot decode key for {}",
|
||||
contact.addr
|
||||
)
|
||||
})
|
||||
.log_err(context)
|
||||
.ok()
|
||||
});
|
||||
if let Some(public_key) = key {
|
||||
let timestamp = contact
|
||||
.timestamp
|
||||
.as_ref()
|
||||
.map_or(0, |&t| min(t, smeared_time(context)));
|
||||
let aheader = Aheader {
|
||||
addr: contact.addr.clone(),
|
||||
public_key,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
};
|
||||
let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
Ok(p) => p,
|
||||
};
|
||||
let peerstate = if let Some(mut p) = peerstate {
|
||||
p.apply_gossip(&aheader, timestamp);
|
||||
p
|
||||
} else {
|
||||
Peerstate::from_gossip(&aheader, timestamp)
|
||||
};
|
||||
if let Err(e) = peerstate.save_to_db(&context.sql).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
if let Err(e) = peerstate
|
||||
.handle_fingerprint_change(context, timestamp)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
if modified != Modifier::Created {
|
||||
return Ok(id);
|
||||
}
|
||||
let path = match &contact.profile_image {
|
||||
Some(image) => match BlobObject::store_from_base64(context, image, "avatar").await {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not decode and save avatar for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(path) => Some(path),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if let Some(path) = path {
|
||||
// Currently this value doesn't matter as we don't import the contact of self.
|
||||
let was_encrypted = false;
|
||||
if let Err(e) =
|
||||
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// An object representing a single contact in memory.
|
||||
///
|
||||
/// The contact object is not updated.
|
||||
@@ -394,6 +547,10 @@ impl Contact {
|
||||
{
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.authname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
@@ -808,7 +965,6 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book) {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(&name);
|
||||
match ContactAddress::new(&addr) {
|
||||
Ok(addr) => {
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
@@ -1383,22 +1539,6 @@ impl Contact {
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Updates the origin of the contact, but only if new origin is higher than the current one.
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
(origin, contact_id, origin),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
@@ -1784,7 +1924,7 @@ impl RecentlySeenLoop {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{may_be_valid_addr, normalize_name};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
@@ -2849,7 +2989,7 @@ Until the false-positive is fixed:
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_make_vcard() -> Result<()> {
|
||||
async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
@@ -2883,8 +3023,8 @@ Until the false-positive is fixed:
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0].addr, bob_addr);
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, Some(key_base64));
|
||||
assert_eq!(contacts[0].profile_image, Some(avatar_base64));
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
@@ -2894,6 +3034,110 @@ Until the false-positive is fixed:
|
||||
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
let contact_ids = import_vcard(alice, &vcard).await?;
|
||||
assert_eq!(contact_ids.len(), 2);
|
||||
for _ in 0..contact_ids.len() {
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_))))
|
||||
.await;
|
||||
}
|
||||
|
||||
let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?;
|
||||
// This should be the same vCard except timestamps, check that roughly.
|
||||
let contacts = contact_tools::parse_vcard(&vcard);
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0].addr, bob_addr);
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
|
||||
let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?;
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// Bob only actually imports Fiona, though `ContactId::SELF` is also returned.
|
||||
bob.evtracker.clear_events();
|
||||
let contact_ids = import_vcard(bob, &vcard).await?;
|
||||
bob.emit_event(EventType::Test);
|
||||
assert_eq!(contact_ids.len(), 2);
|
||||
assert_eq!(contact_ids[0], ContactId::SELF);
|
||||
let ev = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1])));
|
||||
let ev = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::Test);
|
||||
let vcard = make_vcard(bob, &[contact_ids[1]]).await?;
|
||||
let contacts = contact_tools::parse_vcard(&vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "fiona@example.net");
|
||||
assert_eq!(contacts[0].authname, "".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
alice.evtracker.clear_events();
|
||||
let alice_bob_id = import_vcard(alice, &vcard).await?[0];
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id)));
|
||||
let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?;
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let bob = &TestContext::new().await;
|
||||
bob.configure_addr(bob_addr).await;
|
||||
bob.set_config(Config::Displayname, Some("Not Bob")).await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
|
||||
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
|
||||
assert_eq!(alice_bob_contact.get_authname(), "Bob");
|
||||
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
|
||||
let msg = alice.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::contact_setup_changed(alice, bob_addr).await
|
||||
);
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// The old vCard is imported, but doesn't change Bob's key for Alice.
|
||||
import_vcard(alice, &vcard).await?.first().unwrap();
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +498,23 @@ impl Context {
|
||||
self.get_config_bool(Config::IsChatmail).await
|
||||
}
|
||||
|
||||
/// Returns maximum number of recipients the provider allows to send a single email to.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
|
||||
let is_chatmail = self.is_chatmail().await?;
|
||||
let val = self
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or_else(
|
||||
|| match is_chatmail {
|
||||
true => usize::MAX,
|
||||
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
|
||||
},
|
||||
usize::from,
|
||||
);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Does a background fetch
|
||||
/// pauses the scheduler and does one imap fetch, then unpauses and returns
|
||||
pub async fn background_fetch(&self) -> Result<()> {
|
||||
@@ -950,6 +967,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1656,7 +1679,6 @@ mod tests {
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"iroh_secret_key",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
|
||||
@@ -184,7 +184,7 @@ impl Session {
|
||||
bail!("Attempt to fetch UID 0");
|
||||
}
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_folder(context, folder).await?;
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
@@ -71,7 +71,14 @@ impl EventEmitter {
|
||||
/// [`try_recv`]: Self::try_recv
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
let mut lock = self.0.lock().await;
|
||||
lock.recv().await.ok()
|
||||
match lock.recv().await {
|
||||
Err(async_broadcast::RecvError::Overflowed(n)) => Some(Event {
|
||||
id: 0,
|
||||
typ: EventType::EventChannelOverflow { n },
|
||||
}),
|
||||
Err(async_broadcast::RecvError::Closed) => None,
|
||||
Ok(event) => Some(event),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to receive an event without blocking.
|
||||
@@ -86,8 +93,19 @@ impl EventEmitter {
|
||||
// to avoid blocking
|
||||
// in case there is a concurrent call to `recv`.
|
||||
let mut lock = self.0.try_lock()?;
|
||||
let event = lock.try_recv()?;
|
||||
Ok(event)
|
||||
match lock.try_recv() {
|
||||
Err(async_broadcast::TryRecvError::Overflowed(n)) => {
|
||||
// Some events have been lost,
|
||||
// but the channel is not closed.
|
||||
Ok(Event {
|
||||
id: 0,
|
||||
typ: EventType::EventChannelOverflow { n },
|
||||
})
|
||||
}
|
||||
res @ (Err(async_broadcast::TryRecvError::Empty)
|
||||
| Err(async_broadcast::TryRecvError::Closed)
|
||||
| Ok(_)) => Ok(res?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,4 +311,14 @@ pub enum EventType {
|
||||
/// ID of the changed chat
|
||||
chat_id: Option<ChatId>,
|
||||
},
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow {
|
||||
/// Number of events skipped.
|
||||
n: u64,
|
||||
},
|
||||
}
|
||||
|
||||
26
src/imap.rs
26
src/imap.rs
@@ -838,7 +838,7 @@ impl Session {
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msgs = BTreeMap::new();
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_folder(context, folder).await?;
|
||||
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
@@ -1039,7 +1039,7 @@ impl Session {
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_folder(context, folder).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
@@ -1087,7 +1087,7 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
self.select_folder(context, Some(&folder))
|
||||
self.select_folder(context, &folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
@@ -1131,7 +1131,7 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.select_folder(context, Some(folder))
|
||||
self.select_folder(context, folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
@@ -1472,13 +1472,15 @@ impl Session {
|
||||
admin = m.value;
|
||||
}
|
||||
"/shared/vendor/deltachat/irohrelay" => {
|
||||
if let Some(url) = m.value.as_deref().and_then(|s| Url::parse(s).ok()) {
|
||||
iroh_relay = Some(url);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Got invalid URL from iroh relay metadata: {:?}.", m.value
|
||||
);
|
||||
if let Some(value) = m.value {
|
||||
if let Ok(url) = Url::parse(&value) {
|
||||
iroh_relay = Some(url);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Got invalid URL from iroh relay metadata: {:?}.", value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -1561,7 +1563,7 @@ impl Session {
|
||||
) -> Result<Option<&'a str>> {
|
||||
// Close currently selected folder if needed.
|
||||
// We are going to select folders using low-level EXAMINE operations below.
|
||||
self.select_folder(context, None).await?;
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
for folder in folders {
|
||||
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Session {
|
||||
) -> Result<Self> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_folder(context, folder).await?;
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
return Ok(self);
|
||||
|
||||
@@ -55,15 +55,13 @@ impl ImapSession {
|
||||
pub(crate) async fn select_folder(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: Option<&str>,
|
||||
folder: &str,
|
||||
) -> Result<NewlySelected> {
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(folder) = folder {
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,34 +69,30 @@ impl ImapSession {
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
// select new folder
|
||||
if let Some(folder) = folder {
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => Err(Error::Other(err.to_string())),
|
||||
}
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
Ok(NewlySelected::No)
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => Err(Error::Other(err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +102,7 @@ impl ImapSession {
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> anyhow::Result<NewlySelected> {
|
||||
match self.select_folder(context, Some(folder)).await {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => {
|
||||
@@ -118,7 +112,7 @@ impl ImapSession {
|
||||
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
|
||||
// Need to recheck, could have been created in parallel.
|
||||
}
|
||||
let select_res = self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"));
|
||||
let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
|
||||
if select_res.is_err() {
|
||||
create_res?;
|
||||
}
|
||||
|
||||
@@ -1433,7 +1433,7 @@ pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
|
||||
"txt" => (Viewtype::File, "text/plain"),
|
||||
"vcard" => (Viewtype::Vcard, "text/vcard"),
|
||||
"vcf" => (Viewtype::Vcard, "text/vcard"),
|
||||
"wav" => (Viewtype::File, "audio/wav"),
|
||||
"wav" => (Viewtype::Audio, "audio/wav"),
|
||||
"weba" => (Viewtype::File, "audio/webm"),
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
@@ -155,6 +155,7 @@ impl<'a> MimeFactory<'a> {
|
||||
};
|
||||
|
||||
let mut recipients = Vec::with_capacity(5);
|
||||
let mut recipient_ids = HashSet::new();
|
||||
let mut req_mdn = false;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
@@ -169,7 +170,7 @@ impl<'a> MimeFactory<'a> {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
"SELECT c.authname, c.addr, c.id \
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
@@ -177,19 +178,23 @@ impl<'a> MimeFactory<'a> {
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
let id: ContactId = row.get(2)?;
|
||||
Ok((authname, addr, id))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (authname, addr) = row?;
|
||||
let (authname, addr, id) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let recipient_ids: Vec<_> = recipient_ids.into_iter().collect();
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
|
||||
if !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
|
||||
@@ -1587,9 +1587,8 @@ impl MimeMessage {
|
||||
.get_header_value(HeaderDef::MessageId)
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let mut to_list = get_all_addresses_from_header(&report.headers, |header_key| {
|
||||
header_key == "x-failed-recipients"
|
||||
});
|
||||
let mut to_list =
|
||||
get_all_addresses_from_header(&report.headers, "x-failed-recipients");
|
||||
let to = if to_list.len() == 1 {
|
||||
Some(to_list.pop().unwrap())
|
||||
} else {
|
||||
@@ -2056,36 +2055,48 @@ fn get_attachment_filename(
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| {
|
||||
header_key == "to" || header_key == "cc"
|
||||
})
|
||||
let to_addresses = get_all_addresses_from_header(headers, "to");
|
||||
let cc_addresses = get_all_addresses_from_header(headers, "cc");
|
||||
|
||||
let mut res = to_addresses;
|
||||
res.extend(cc_addresses);
|
||||
res
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
|
||||
let all = get_all_addresses_from_header(headers, |header_key| header_key == "from");
|
||||
let all = get_all_addresses_from_header(headers, "from");
|
||||
tools::single_value(all)
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "list-post")
|
||||
get_all_addresses_from_header(headers, "list-post")
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|s| s.addr)
|
||||
}
|
||||
|
||||
fn get_all_addresses_from_header(
|
||||
headers: &[MailHeader],
|
||||
pred: fn(String) -> bool,
|
||||
) -> Vec<SingleInfo> {
|
||||
/// Extracts all addresses from the header named `header`.
|
||||
///
|
||||
/// If multiple headers with the same name are present,
|
||||
/// the last one is taken.
|
||||
/// This is because DKIM-Signatures apply to the last
|
||||
/// headers, and more headers may be added
|
||||
/// to the beginning of the messages
|
||||
/// without invalidating the signature
|
||||
/// unless the header is "oversigned",
|
||||
/// i.e. included in the signature more times
|
||||
/// than it appears in the mail.
|
||||
fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
|
||||
let mut result: Vec<SingleInfo> = Default::default();
|
||||
|
||||
headers
|
||||
if let Some(header) = headers
|
||||
.iter()
|
||||
.filter(|header| pred(header.get_key().to_lowercase()))
|
||||
.filter_map(|header| mailparse::addrparse_header(header).ok())
|
||||
.for_each(|addrs| {
|
||||
.rev()
|
||||
.find(|h| h.get_key().to_lowercase() == header)
|
||||
{
|
||||
if let Ok(addrs) = mailparse::addrparse_header(header) {
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(ref info) => {
|
||||
@@ -2104,7 +2115,8 @@ fn get_all_addresses_from_header(
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -2404,6 +2416,22 @@ mod tests {
|
||||
assert!(recipients.iter().any(|info| info.addr == "abc@bcd.com"));
|
||||
assert!(recipients.iter().any(|info| info.addr == "def@def.de"));
|
||||
assert_eq!(recipients.len(), 2);
|
||||
|
||||
// If some header is present multiple times,
|
||||
// only the last one must be used.
|
||||
let raw = b"From: alice@example.org\n\
|
||||
TO: mallory@example.com\n\
|
||||
To: mallory@example.net\n\
|
||||
To: bob@example.net\n\
|
||||
Content-Type: text/plain\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n\
|
||||
";
|
||||
let mail = mailparse::parse_mail(&raw[..]).unwrap();
|
||||
let recipients = get_recipients(&mail.headers);
|
||||
assert!(recipients.iter().any(|info| info.addr == "bob@example.net"));
|
||||
assert_eq!(recipients.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3986,4 +4014,43 @@ Content-Type: text/plain; charset=utf-8
|
||||
// Not "Some subject – /help"
|
||||
assert_eq!(message.parts[0].msg, "/help");
|
||||
}
|
||||
|
||||
/// Tests that Delta Chat takes the last header value
|
||||
/// rather than the first one if multiple headers
|
||||
/// are present.
|
||||
///
|
||||
/// DKIM signature applies to the last N headers
|
||||
/// if header name is included N times in
|
||||
/// DKIM-Signature.
|
||||
///
|
||||
/// If the client takes the first header
|
||||
/// rather than the last, it can be fooled
|
||||
/// into using unsigned header
|
||||
/// when signed one is present
|
||||
/// but not protected by oversigning.
|
||||
///
|
||||
/// See
|
||||
/// <https://www.zone.eu/blog/2024/05/17/bimi-and-dmarc-cant-save-you/>
|
||||
/// for reference.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_take_last_header() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// Mallory added second From: header.
|
||||
let raw = b"From: mallory@example.org\n\
|
||||
From: alice@example.org\n\
|
||||
Content-Type: text/plain\n\
|
||||
Chat-Version: 1.0\n\
|
||||
\n\
|
||||
Hello\n\
|
||||
";
|
||||
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mimeparser.get_header(HeaderDef::From_).unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use email::Header;
|
||||
use iroh_gossip::net::{Gossip, JoinTopicFut, GOSSIP_ALPN};
|
||||
use iroh_gossip::proto::{Event as IrohEvent, TopicId};
|
||||
use iroh_net::relay::{RelayMap, RelayUrl};
|
||||
use iroh_net::{key::SecretKey, relay::RelayMode, MagicEndpoint};
|
||||
use iroh_net::{key::SecretKey, relay::RelayMode, Endpoint};
|
||||
use iroh_net::{NodeAddr, NodeId};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::env;
|
||||
@@ -37,6 +37,7 @@ use tokio::task::JoinHandle;
|
||||
use url::Url;
|
||||
|
||||
use crate::chat::send_msg;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
@@ -50,8 +51,8 @@ const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
/// Store iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
pub struct Iroh {
|
||||
/// [MagicEndpoint] needed for iroh peer channels.
|
||||
pub(crate) endpoint: MagicEndpoint,
|
||||
/// [Endpoint] needed for iroh peer channels.
|
||||
pub(crate) endpoint: Endpoint,
|
||||
|
||||
/// [Gossip] needed for iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
@@ -80,7 +81,14 @@ impl Iroh {
|
||||
msg_id: MsgId,
|
||||
) -> Result<Option<JoinTopicFut>> {
|
||||
let topic = get_iroh_topic_for_msg(ctx, msg_id).await?;
|
||||
let seq = if let Some(channel_state) = self.iroh_channels.read().await.get(&topic) {
|
||||
|
||||
// Take exclusive lock to make sure
|
||||
// no other thread can create a second gossip subscription
|
||||
// after we check that it does not exist and before we create a new one.
|
||||
// Otherwise we would receive every message twice or more times.
|
||||
let mut iroh_channels = self.iroh_channels.write().await;
|
||||
|
||||
let seq = if let Some(channel_state) = iroh_channels.get(&topic) {
|
||||
if channel_state.subscribe_loop.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -114,14 +122,26 @@ impl Iroh {
|
||||
}
|
||||
});
|
||||
|
||||
self.iroh_channels
|
||||
.write()
|
||||
.await
|
||||
.insert(topic, ChannelState::new(seq, subscribe_loop));
|
||||
iroh_channels.insert(topic, ChannelState::new(seq, subscribe_loop));
|
||||
|
||||
Ok(Some(connect_future))
|
||||
}
|
||||
|
||||
/// Add gossip peers to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
|
||||
if let Some(state) = self.iroh_channels.read().await.get(&topic) {
|
||||
if state.subscribe_loop.is_some() {
|
||||
for peer in &peers {
|
||||
self.endpoint.add_node_addr(peer.clone())?;
|
||||
}
|
||||
self.gossip
|
||||
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send realtime data to the gossip swarm.
|
||||
pub async fn send_webxdc_realtime_data(
|
||||
&self,
|
||||
@@ -210,7 +230,7 @@ impl Context {
|
||||
RelayMode::Default
|
||||
};
|
||||
|
||||
let endpoint = MagicEndpoint::builder()
|
||||
let endpoint = Endpoint::builder()
|
||||
.secret_key(secret_key.clone())
|
||||
.alpns(vec![GOSSIP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
@@ -319,6 +339,10 @@ pub async fn send_webxdc_realtime_advertisement(
|
||||
ctx: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<Option<JoinTopicFut>> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
let conn = iroh.join_and_subscribe_gossip(ctx, msg_id).await?;
|
||||
|
||||
@@ -332,8 +356,12 @@ pub async fn send_webxdc_realtime_advertisement(
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Send realtime data to the gossip swarm.
|
||||
/// Send realtime data to other peers using iroh.
|
||||
pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u8>) -> Result<()> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
iroh.send_webxdc_realtime_data(ctx, msg_id, data).await?;
|
||||
Ok(())
|
||||
@@ -341,6 +369,10 @@ pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u
|
||||
|
||||
/// Leave the gossip of the webxdc with given [MsgId].
|
||||
pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
iroh.leave_realtime(get_iroh_topic_for_msg(ctx, msg_id).await?)
|
||||
.await?;
|
||||
@@ -365,7 +397,7 @@ pub(crate) async fn create_iroh_header(
|
||||
))
|
||||
}
|
||||
|
||||
async fn endpoint_loop(context: Context, endpoint: MagicEndpoint, gossip: Gossip) {
|
||||
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
|
||||
while let Some(conn) = endpoint.accept().await {
|
||||
info!(context, "IROH_REALTIME: accepting iroh connection");
|
||||
let gossip = gossip.clone();
|
||||
@@ -380,12 +412,12 @@ async fn endpoint_loop(context: Context, endpoint: MagicEndpoint, gossip: Gossip
|
||||
|
||||
async fn handle_connection(
|
||||
context: &Context,
|
||||
mut conn: iroh_net::magic_endpoint::Connecting,
|
||||
mut conn: iroh_net::endpoint::Connecting,
|
||||
gossip: Gossip,
|
||||
) -> anyhow::Result<()> {
|
||||
let alpn = conn.alpn().await?;
|
||||
let conn = conn.await?;
|
||||
let peer_id = iroh_net::magic_endpoint::get_remote_node_id(&conn)?;
|
||||
let peer_id = iroh_net::endpoint::get_remote_node_id(&conn)?;
|
||||
|
||||
match alpn.as_bytes() {
|
||||
GOSSIP_ALPN => gossip
|
||||
@@ -432,13 +464,10 @@ async fn subscribe_loop(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
chat::send_msg,
|
||||
message::{Message, Viewtype},
|
||||
peer_channels::{
|
||||
get_iroh_gossip_peers, get_iroh_topic_for_msg, leave_webxdc_realtime,
|
||||
send_webxdc_realtime_advertisement,
|
||||
},
|
||||
test_utils::TestContextManager,
|
||||
EventType,
|
||||
};
|
||||
@@ -449,6 +478,17 @@ 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);
|
||||
@@ -580,6 +620,21 @@ 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
|
||||
.unwrap());
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
@@ -708,4 +763,109 @@ mod tests {
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parallel_connect() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
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);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
let alice_webxdc = alice.get_last_msg().await;
|
||||
|
||||
let webxdc = alice.pop_sent_msg().await;
|
||||
let bob_webxdc = bob.recv_msg(&webxdc).await;
|
||||
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
|
||||
|
||||
bob_webxdc.chat_id.accept(bob).await.unwrap();
|
||||
|
||||
eprintln!("Sending advertisements");
|
||||
// Alice advertises herself.
|
||||
let alice_advertisement_future = send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_advertisement = alice.pop_sent_msg().await;
|
||||
|
||||
send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_advertisement = bob.pop_sent_msg().await;
|
||||
|
||||
eprintln!("Receiving advertisements");
|
||||
bob.recv_msg_trash(&alice_advertisement).await;
|
||||
alice.recv_msg_trash(&bob_advertisement).await;
|
||||
|
||||
eprintln!("Alice waits for connection");
|
||||
alice_advertisement_future.await.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
eprintln!("Sending ephemeral message");
|
||||
send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
eprintln!("Waiting for ephemeral message");
|
||||
loop {
|
||||
let event = bob.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == b"alice -> bob" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
"Unexpected status update: {}",
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_peer_channels_disabled() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.get().is_none())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,10 +538,18 @@ impl Peerstate {
|
||||
if let Some(old_addr) = old_addr {
|
||||
// We are doing an AEAP transition to the new address and the SQL INSERT below will
|
||||
// save the existing peerstate as belonging to this new address. We now need to
|
||||
// delete the peerstate that belongs to the current address in case if the contact
|
||||
// later wants to move back to the current address. Otherwise the old entry will be
|
||||
// just found and updated instead of doing AEAP.
|
||||
t.execute("DELETE FROM acpeerstates WHERE addr=?", (old_addr,))?;
|
||||
// "unverify" the peerstate that belongs to the current address in case if the
|
||||
// contact later wants to move back to the current address. Otherwise the old entry
|
||||
// will be just found and updated instead of doing AEAP. We can't just delete the
|
||||
// existing peerstate as this would break encryption to it. This is critical for
|
||||
// non-verified groups -- if we can't encrypt to the old address, we can't securely
|
||||
// remove it from the group (to add the new one instead).
|
||||
t.execute(
|
||||
"UPDATE acpeerstates \
|
||||
SET verified_key=NULL, verified_key_fingerprint='', verifier='' \
|
||||
WHERE addr=?",
|
||||
(old_addr,),
|
||||
)?;
|
||||
}
|
||||
t.execute(
|
||||
"INSERT INTO acpeerstates (
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid};
|
||||
use crate::tools::{self, buf_compress};
|
||||
use crate::{chatlist_events, location};
|
||||
use crate::{contact, imap};
|
||||
use iroh_net::NodeAddr;
|
||||
@@ -769,11 +769,7 @@ async fn add_parts(
|
||||
if mime_parser.incoming {
|
||||
to_id = ContactId::SELF;
|
||||
|
||||
let test_normal_chat = if from_id == ContactId::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
ChatIdBlocked::lookup_by_contact(context, from_id).await?
|
||||
};
|
||||
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
|
||||
|
||||
if chat_id.is_none() && mime_parser.delivery_report.is_some() {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -786,29 +782,6 @@ async fn add_parts(
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
// Try to assign to a chat based on Chat-Group-ID.
|
||||
if chat_id.is_none() {
|
||||
if let Some(grpid) = mime_parser.get_chat_group_id() {
|
||||
if let Some((id, _protected, blocked)) =
|
||||
chat::get_chat_id_by_grpid(context, grpid).await?
|
||||
{
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
lookup_chat_by_reply(context, mime_parser, &parent, to_ids, from_id).await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
// signals whether the current user is a bot
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
|
||||
@@ -838,22 +811,44 @@ async fn add_parts(
|
||||
create_blocked_default
|
||||
};
|
||||
|
||||
if chat_id.is_none() && !is_mdn {
|
||||
// try to create a group
|
||||
// Try to assign to a chat based on Chat-Group-ID.
|
||||
if chat_id.is_none() {
|
||||
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
|
||||
if let Some((id, _protected, blocked)) =
|
||||
chat::get_chat_id_by_grpid(context, &grpid).await?
|
||||
{
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
} else if allow_creation || test_normal_chat.is_some() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
&verified_encryption,
|
||||
&grpid,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
if chat_id.is_none() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
if test_normal_chat.is_none() {
|
||||
allow_creation
|
||||
} else {
|
||||
true
|
||||
},
|
||||
create_blocked,
|
||||
from_id,
|
||||
&parent,
|
||||
to_ids,
|
||||
&verified_encryption,
|
||||
from_id,
|
||||
allow_creation || test_normal_chat.is_some(),
|
||||
create_blocked,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -871,7 +866,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
// In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat
|
||||
// In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat
|
||||
// but the From-address is not a member of this chat.
|
||||
if let Some(group_chat_id) = chat_id {
|
||||
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
|
||||
@@ -964,7 +959,7 @@ async fn add_parts(
|
||||
if create_blocked == Blocked::Request && parent.is_some() {
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
// the contact requests will pop up and this should be just fine.
|
||||
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
|
||||
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
@@ -1055,27 +1050,32 @@ async fn add_parts(
|
||||
|
||||
// Try to assign to a chat based on Chat-Group-ID.
|
||||
if chat_id.is_none() {
|
||||
if let Some(grpid) = mime_parser.get_chat_group_id() {
|
||||
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
|
||||
if let Some((id, _protected, blocked)) =
|
||||
chat::get_chat_id_by_grpid(context, grpid).await?
|
||||
chat::get_chat_id_by_grpid(context, &grpid).await?
|
||||
{
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
} else if allow_creation {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
Blocked::Not,
|
||||
from_id,
|
||||
to_ids,
|
||||
&verified_encryption,
|
||||
&grpid,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
lookup_chat_by_reply(context, mime_parser, &parent, to_ids, from_id).await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.decrypting_failed && !fetching_existing_messages {
|
||||
if chat_id.is_none() {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -1107,24 +1107,25 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if !to_ids.is_empty() {
|
||||
if chat_id.is_none() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
from_id,
|
||||
to_ids,
|
||||
&verified_encryption,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
if chat_id.is_none() {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
|
||||
context,
|
||||
mime_parser,
|
||||
&parent,
|
||||
to_ids,
|
||||
from_id,
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
if !to_ids.is_empty() {
|
||||
if chat_id.is_none() && allow_creation {
|
||||
let to_contact = Contact::get_by_id(context, to_id).await?;
|
||||
if let Some(list_id) = to_contact.param.get(Param::ListId) {
|
||||
@@ -1443,7 +1444,8 @@ async fn add_parts(
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
let topic = get_iroh_topic_for_msg(context, instance_id).await?;
|
||||
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?;
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1788,6 +1790,44 @@ async fn lookup_chat_by_reply(
|
||||
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn lookup_chat_or_create_adhoc_group(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
parent: &Option<Message>,
|
||||
to_ids: &[ContactId],
|
||||
from_id: ContactId,
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
is_partial_download: bool,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) =
|
||||
// Try to assign to a chat based on In-Reply-To/References.
|
||||
lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
|
||||
{
|
||||
Ok(Some((new_chat_id, new_chat_id_blocked)))
|
||||
} else if allow_creation {
|
||||
// Try to create an ad hoc group.
|
||||
if let Some(new_chat_id) = create_adhoc_group(
|
||||
context,
|
||||
mime_parser,
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
is_partial_download,
|
||||
)
|
||||
.await
|
||||
.context("Could not create ad hoc group")?
|
||||
{
|
||||
Ok(Some((new_chat_id, create_blocked)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
|
||||
/// If it returns false, it shall be assigned to the parent chat.
|
||||
async fn is_probably_private_reply(
|
||||
@@ -1825,61 +1865,24 @@ async fn is_probably_private_reply(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// This function tries to extract the group-id from the message and returns the corresponding
|
||||
/// chat_id. If the chat does not exist, it is created. If there is no group-id and there are more
|
||||
/// This function tries to extract the group-id from the message and create a new group
|
||||
/// chat with this ID. If there is no group-id and there are more
|
||||
/// than two members, a new ad hoc group is created.
|
||||
///
|
||||
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
|
||||
/// On success the function returns the created (chat_id, chat_blocked) tuple.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_or_lookup_group(
|
||||
async fn create_group(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
is_partial_download: bool,
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
verified_encryption: &VerifiedEncryption,
|
||||
grpid: &str,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
|
||||
grpid
|
||||
} else if !allow_creation {
|
||||
info!(context, "Creating ad-hoc group prevented from caller.");
|
||||
return Ok(None);
|
||||
} else if is_partial_download {
|
||||
// Partial download may be an encrypted message with protected Subject header.
|
||||
//
|
||||
// We do not want to create a group with "..." or "Encrypted message" as a subject.
|
||||
info!(
|
||||
context,
|
||||
"Ad-hoc group cannot be created from partial download."
|
||||
);
|
||||
return Ok(None);
|
||||
} else {
|
||||
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
|
||||
if !member_ids.contains(&(from_id)) {
|
||||
member_ids.push(from_id);
|
||||
}
|
||||
if !member_ids.contains(&(ContactId::SELF)) {
|
||||
member_ids.push(ContactId::SELF);
|
||||
}
|
||||
|
||||
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
|
||||
.await
|
||||
.context("could not create ad hoc group")?
|
||||
.map(|chat_id| (chat_id, create_blocked));
|
||||
return Ok(res);
|
||||
};
|
||||
|
||||
let mut chat_id;
|
||||
let mut chat_id_blocked;
|
||||
if let Some((id, _protected, blocked)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
|
||||
chat_id = Some(id);
|
||||
chat_id_blocked = blocked;
|
||||
} else {
|
||||
chat_id = None;
|
||||
chat_id_blocked = Default::default();
|
||||
}
|
||||
let mut chat_id = None;
|
||||
let mut chat_id_blocked = Default::default();
|
||||
|
||||
// For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
|
||||
// they belong to the group because of the Chat-Group-Id or Message-Id header
|
||||
@@ -1920,15 +1923,10 @@ async fn create_or_lookup_group(
|
||||
// otherwise, a pending "quit" message may pop up
|
||||
&& mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
|
||||
// re-create explicitly left groups only if ourself is re-added
|
||||
&& (!chat::is_group_explicitly_left(context, &grpid).await?
|
||||
&& (!chat::is_group_explicitly_left(context, grpid).await?
|
||||
|| self_explicitly_added(context, &mime_parser).await?)
|
||||
{
|
||||
// Group does not exist but should be created.
|
||||
if !allow_creation {
|
||||
info!(context, "Creating group forbidden by caller.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let grpname = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.context("Chat-Group-Name vanished")?
|
||||
@@ -1938,7 +1936,7 @@ async fn create_or_lookup_group(
|
||||
let new_chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
&grpid,
|
||||
grpid,
|
||||
grpname,
|
||||
create_blocked,
|
||||
create_protected,
|
||||
@@ -2491,44 +2489,34 @@ async fn apply_mailinglist_changes(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_getting_grpid(mime_parser: &MimeMessage) -> Option<String> {
|
||||
if let Some(optional_field) = mime_parser.get_chat_group_id() {
|
||||
return Some(optional_field.to_string());
|
||||
}
|
||||
|
||||
// Useful for undecipherable messages sent to known group.
|
||||
if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::MessageId) {
|
||||
return Some(extracted_grpid.to_string());
|
||||
}
|
||||
|
||||
if !mime_parser.has_chat_version() {
|
||||
if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::InReplyTo) {
|
||||
return Some(extracted_grpid.to_string());
|
||||
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::References) {
|
||||
return Some(extracted_grpid.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// try extract a grpid from a message-id list header value
|
||||
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
|
||||
let header = mime_parser.get_header(headerdef)?;
|
||||
let parts = header
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|part| !part.is_empty());
|
||||
parts.filter_map(extract_grpid_from_rfc724_mid).next()
|
||||
}
|
||||
|
||||
/// Creates ad-hoc group and returns chat ID on success.
|
||||
async fn create_adhoc_group(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
create_blocked: Blocked,
|
||||
member_ids: &[ContactId],
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
is_partial_download: bool,
|
||||
) -> Result<Option<ChatId>> {
|
||||
if is_partial_download {
|
||||
// Partial download may be an encrypted message with protected Subject header.
|
||||
//
|
||||
// We do not want to create a group with "..." or "Encrypted message" as a subject.
|
||||
info!(
|
||||
context,
|
||||
"Ad-hoc group cannot be created from partial download."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
|
||||
if !member_ids.contains(&(from_id)) {
|
||||
member_ids.push(from_id);
|
||||
}
|
||||
if !member_ids.contains(&(ContactId::SELF)) {
|
||||
member_ids.push(ContactId::SELF);
|
||||
}
|
||||
|
||||
if mime_parser.is_mailinglist_message() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -2574,7 +2562,7 @@ async fn create_adhoc_group(
|
||||
context,
|
||||
"Created ad-hoc group id={new_chat_id}, name={grpname:?}."
|
||||
);
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &member_ids).await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
@@ -16,25 +16,6 @@ use crate::imex::{imex, ImexMode};
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_grpid_simple() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello@example.org\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <lqkjwelq123@123123>\n\
|
||||
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
|
||||
\n\
|
||||
hello\x00";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(mimeparser.incoming, true);
|
||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None);
|
||||
let grpid = Some("HcxyMARjyJy");
|
||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
@@ -61,24 +42,6 @@ async fn test_bad_from() {
|
||||
assert!(mimeparser.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_grpid_from_multiple() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: hello@example.org\n\
|
||||
Subject: outer-subject\n\
|
||||
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
|
||||
References: <qweqweqwe>, <Gr.HcxyMARjyJy.9-uvzWPTLtV@nau.ca>\n\
|
||||
\n\
|
||||
hello\x00";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
let grpid = Some("HcxyMARjyJy");
|
||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid);
|
||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
|
||||
}
|
||||
|
||||
static MSGRMSG: &[u8] =
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
@@ -1857,26 +1820,31 @@ async fn test_save_mime_headers_on() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestContext, TestContext) {
|
||||
async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: bool) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
// Claire, a customer, sends a support request
|
||||
// to the alias address <support@example.org> from a classic MUA.
|
||||
// to the alias address <support@example.org>.
|
||||
// If `chat_request` is true, Claire is using Delta Chat,
|
||||
// otherwise Claire sends the request from a classic MUA.
|
||||
// The alias expands to the supporters Alice and Bob.
|
||||
// Check that Alice receives the message in a group chat.
|
||||
let claire_request = if group_request {
|
||||
format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
To: support@example.org, ceo@example.org\n\
|
||||
From: claire@example.org\n\
|
||||
Subject: i have a question\n\
|
||||
Message-ID: <non-dc-1@example.org>\n\
|
||||
{}\
|
||||
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi support! what is the current version?",
|
||||
To: support@example.org, ceo@example.org\n\
|
||||
From: claire@example.org\n\
|
||||
Subject: i have a question\n\
|
||||
Message-ID: <non-dc-1@example.org>\n\
|
||||
{}\
|
||||
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi support! what is the current version?",
|
||||
if chat_request {
|
||||
"Chat-Group-ID: 8ud29aridt29arid\n\
|
||||
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n"
|
||||
"Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: 8ud29aridt29arid\n\
|
||||
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -1884,15 +1852,15 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
|
||||
} else {
|
||||
format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
To: support@example.org\n\
|
||||
From: claire@example.org\n\
|
||||
Subject: i have a question\n\
|
||||
Message-ID: <non-dc-1@example.org>\n\
|
||||
{}\
|
||||
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi support! what is the current version?",
|
||||
To: support@example.org\n\
|
||||
From: claire@example.org\n\
|
||||
Subject: i have a question\n\
|
||||
Message-ID: <non-dc-1@example.org>\n\
|
||||
{}\
|
||||
Date: Sun, 14 Mar 2021 17:04:36 +0100\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi support! what is the current version?",
|
||||
if chat_request {
|
||||
"Chat-Version: 1.0\n"
|
||||
} else {
|
||||
@@ -1901,11 +1869,11 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
|
||||
)
|
||||
};
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
receive_imf(&alice, claire_request.as_bytes(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that Alice receives the message in a group chat.
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_subject(), "i have a question");
|
||||
assert!(msg.get_text().contains("hi support!"));
|
||||
@@ -1919,7 +1887,7 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
|
||||
}
|
||||
assert_eq!(msg.get_override_sender_name(), None);
|
||||
|
||||
let claire = TestContext::new().await;
|
||||
let claire = tcm.unconfigured().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
receive_imf(&claire, claire_request.as_bytes(), false)
|
||||
.await
|
||||
@@ -1943,15 +1911,48 @@ async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestCont
|
||||
assert_eq!(get_chat_msgs(&claire, chat.id).await.unwrap().len(), 1);
|
||||
assert_eq!(msg.get_override_sender_name(), None);
|
||||
|
||||
(claire, alice)
|
||||
}
|
||||
|
||||
async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) {
|
||||
let (claire, alice) = create_test_alias(chat_request, group_request).await;
|
||||
let reply = if from_dc {
|
||||
// Bob, the other supporter, answers with Delta Chat.
|
||||
format!(
|
||||
"To: support@example.org, claire@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
|
||||
References: <bobreply@localhost>\n\
|
||||
In-Reply-To: <non-dc-1@example.org>\n\
|
||||
Message-ID: <bobreply@localhost>\n\
|
||||
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
{}\
|
||||
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\
|
||||
Chat-Disposition-Notification-To: bob@example.net\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi claire, the version is 1.0, cheers bob",
|
||||
if group_request && chat_request {
|
||||
"Chat-Group-ID: 8ud29aridt29arid\n"
|
||||
} else {
|
||||
// Ad-hoc group has no ID.
|
||||
""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Bob, the other supporter, answers with a classic MUA.
|
||||
"To: support@example.org, claire@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
|
||||
References: <non-dc-1@example.org>\n\
|
||||
In-Reply-To: <non-dc-1@example.org>\n\
|
||||
Message-ID: <non-dc-2@example.net>\n\
|
||||
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi claire, the version is 1.0, cheers bob"
|
||||
.to_string()
|
||||
};
|
||||
|
||||
// Check that Alice gets the message in the same chat.
|
||||
let request = alice.get_last_msg().await;
|
||||
receive_imf(&alice, reply, false).await.unwrap();
|
||||
receive_imf(&alice, reply.as_bytes(), false).await.unwrap();
|
||||
let answer = alice.get_last_msg().await;
|
||||
assert_eq!(answer.get_subject(), "Re: i have a question");
|
||||
assert!(answer.get_text().contains("the version is 1.0"));
|
||||
@@ -1974,7 +1975,7 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
|
||||
|
||||
// Check that Claire also gets the message in the same chat.
|
||||
let request = claire.get_last_msg().await;
|
||||
receive_imf(&claire, reply, false).await.unwrap();
|
||||
receive_imf(&claire, reply.as_bytes(), false).await.unwrap();
|
||||
let answer = claire.get_last_msg().await;
|
||||
assert_eq!(answer.get_subject(), "Re: i have a question");
|
||||
assert!(answer.get_text().contains("the version is 1.0"));
|
||||
@@ -1986,47 +1987,36 @@ async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_nondc() {
|
||||
// Bob, the other supporter, answers with a classic MUA.
|
||||
let bob_answer = b"To: support@example.org, claire@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
|
||||
References: <non-dc-1@example.org>\n\
|
||||
In-Reply-To: <non-dc-1@example.org>\n\
|
||||
Message-ID: <non-dc-2@example.net>\n\
|
||||
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi claire, the version is 1.0, cheers bob";
|
||||
|
||||
check_alias_reply(bob_answer, true, true).await;
|
||||
check_alias_reply(bob_answer, false, true).await;
|
||||
check_alias_reply(bob_answer, true, false).await;
|
||||
check_alias_reply(bob_answer, false, false).await;
|
||||
async fn test_alias_support_answer_from_nondc_nonchat_nongroup() {
|
||||
check_alias_reply(false, false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_answer_from_dc() {
|
||||
// Bob, the other supporter, answers with Delta Chat.
|
||||
let bob_answer = b"To: support@example.org, claire@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\
|
||||
References: <Gr.af9e810c9b592927.gNm8dVdkZsH@example.net>\n\
|
||||
In-Reply-To: <non-dc-1@example.org>\n\
|
||||
Message-ID: <Gr.af9e810c9b592927.gNm8dVdkZsH@example.net>\n\
|
||||
Date: Sun, 14 Mar 2021 16:04:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: af9e810c9b592927\n\
|
||||
Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\
|
||||
Chat-Disposition-Notification-To: bob@example.net\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hi claire, the version is 1.0, cheers bob";
|
||||
|
||||
check_alias_reply(bob_answer, true, true).await;
|
||||
check_alias_reply(bob_answer, false, true).await;
|
||||
check_alias_reply(bob_answer, true, false).await;
|
||||
check_alias_reply(bob_answer, false, false).await;
|
||||
async fn test_alias_support_answer_from_nondc_nonchat_group() {
|
||||
check_alias_reply(false, false, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_nondc_chat_nongroup() {
|
||||
check_alias_reply(false, true, false).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_nondc_chat_group() {
|
||||
check_alias_reply(false, true, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_dc_nonchat_nongroup() {
|
||||
check_alias_reply(true, false, false).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_dc_nonchat_group() {
|
||||
check_alias_reply(true, false, true).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_dc_chat_nongroup() {
|
||||
check_alias_reply(true, true, false).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_alias_support_answer_from_dc_chat_group() {
|
||||
check_alias_reply(true, true, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -3250,6 +3240,29 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_as_bot() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_id = Contact::create(alice, "", &bob_addr).await?;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
let alice_chat_id = ChatId::lookup_by_contact(alice, alice_bob_id)
|
||||
.await?
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(alice_chat_id).await;
|
||||
assert!(msg.is_bot());
|
||||
let msg = bob.get_last_msg_in(bob_chat_id).await;
|
||||
assert!(msg.is_bot());
|
||||
chat::forward_msgs(bob, &[msg.id], bob_chat_id).await?;
|
||||
let msg = bob.get_last_msg_in(bob_chat_id).await;
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(!msg.is_bot());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4417,7 +4430,7 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
|
||||
// Bob fully reives the partial message.
|
||||
// Bob fully receives the partial message.
|
||||
let msg_id = receive_imf_from_inbox(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
@@ -4607,3 +4620,83 @@ async fn test_receive_vcard() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_no_recipients() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
let raw = b"From: alice@example.org\n\
|
||||
Subject: Group\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-Name: Group\n\
|
||||
Chat-Group-ID: GePFDkwEj2K\n\
|
||||
Message-ID: <foobar@localhost>\n\
|
||||
\n\
|
||||
Hello!";
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
|
||||
let chat = Chat::load_from_db(t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that creating a group
|
||||
/// is preferred over assigning message to existing
|
||||
/// chat based on `In-Reply-To` and `References`.
|
||||
///
|
||||
/// Referenced message itself may be incorrectly assigned,
|
||||
/// but `Chat-Group-ID` uniquely identifies the chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prefer_chat_group_id_over_references() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
// Alice receives 1:1 message from Bob.
|
||||
let raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Hi\n\
|
||||
Message-ID: <oneoneone@localhost>\n\
|
||||
\n\
|
||||
Hello!";
|
||||
receive_imf(t, raw, false).await?.unwrap();
|
||||
|
||||
// Alice receives a group message from Bob.
|
||||
// This references 1:1 message,
|
||||
// but should create a group.
|
||||
let raw = b"From: bob@example.net\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Group\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-Name: Group 1\n\
|
||||
Chat-Group-ID: GePFDkwEj2K\n\
|
||||
Message-ID: <incoming@localhost>\n\
|
||||
References: <oneoneone@localhost>\n\
|
||||
In-Reply-To: <oneoneone@localhost>\n\
|
||||
\n\
|
||||
Group 1";
|
||||
let received1 = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg1 = Message::load_from_db(t, *received1.msg_ids.last().unwrap()).await?;
|
||||
let chat1 = Chat::load_from_db(t, msg1.chat_id).await?;
|
||||
assert_eq!(chat1.typ, Chattype::Group);
|
||||
|
||||
// Alice receives outgoing group message.
|
||||
// This references 1:1 message,
|
||||
// but should create another group.
|
||||
let raw = b"From: alice@example.org\n\
|
||||
To: bob@example.net
|
||||
Subject: Group\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-Name: Group 2\n\
|
||||
Chat-Group-ID: Abcdexyzfoo\n\
|
||||
Message-ID: <outgoing@localhost>\n\
|
||||
References: <oneoneone@localhost>\n\
|
||||
In-Reply-To: <oneoneone@localhost>\n\
|
||||
\n\
|
||||
Group 2";
|
||||
let received2 = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg2 = Message::load_from_db(t, *received2.msg_ids.last().unwrap()).await?;
|
||||
let chat2 = Chat::load_from_db(t, msg2.chat_id).await?;
|
||||
assert_eq!(chat2.typ, Chattype::Group);
|
||||
|
||||
assert_ne!(chat1.id, chat2.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ async fn inbox_loop(
|
||||
} else {
|
||||
match connection.prepare(&ctx).await {
|
||||
Err(err) => {
|
||||
warn!(ctx, "Failed to prepare connection: {:#}.", err);
|
||||
warn!(ctx, "Failed to prepare INBOX connection: {:#}.", err);
|
||||
continue;
|
||||
}
|
||||
Ok(session) => session,
|
||||
@@ -714,7 +714,10 @@ async fn simple_imap_loop(
|
||||
} else {
|
||||
match connection.prepare(&ctx).await {
|
||||
Err(err) => {
|
||||
warn!(ctx, "Failed to prepare connection: {:#}.", err);
|
||||
warn!(
|
||||
ctx,
|
||||
"Failed to prepare {folder_meaning} connection: {err:#}."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(session) => session,
|
||||
|
||||
@@ -447,7 +447,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
contact_id.regossip_keys(context).await?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress(context, contact_id, 600);
|
||||
@@ -757,9 +757,9 @@ mod tests {
|
||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::remove_contact_from_chat;
|
||||
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
@@ -774,6 +774,7 @@ mod tests {
|
||||
Normal,
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -791,6 +792,11 @@ mod tests {
|
||||
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wait_timeout() {
|
||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -850,9 +856,21 @@ mod tests {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::SecurejoinWait)
|
||||
);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
@@ -986,12 +1004,36 @@ mod tests {
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg = get_chat_msg(&bob, chat.get_id(), 0, 2).await;
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
||||
assert_eq!(
|
||||
bob_chat
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Check Bob got expected info messages in his 1:1 chat.
|
||||
let msg_cnt: usize = match case {
|
||||
SetupContactCase::SecurejoinWaitTimeout => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
let msg = get_chat_msg(&bob, chat.get_id(), 1, 2).await;
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::securejoin_wait_timeout(&bob).await
|
||||
);
|
||||
}
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use super::qrinvite::QrInvite;
|
||||
use super::{encrypted_and_signed, verify_sender_by_fingerprint};
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::contact::{ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
@@ -326,8 +326,12 @@ impl BobState {
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined)
|
||||
.await?;
|
||||
ContactId::scaleup_origin(
|
||||
context,
|
||||
&[self.invite.contact_id()],
|
||||
Origin::SecurejoinJoined,
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
self.update_next(&context.sql, SecureJoinStep::Completed)
|
||||
|
||||
36
src/smtp.rs
36
src/smtp.rs
@@ -830,20 +830,26 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
.await
|
||||
.context("failed to update MDN retries count")?;
|
||||
|
||||
let res = send_mdn_msg_id(context, msg_id, contact_id, smtp).await;
|
||||
if let Err(ref err) = res {
|
||||
// If there is an error, for example there is no message corresponding to the msg_id in the
|
||||
// database, do not try to send this MDN again.
|
||||
warn!(
|
||||
context,
|
||||
"Error sending MDN for {msg_id}, removing it: {err:#}."
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.await?;
|
||||
match send_mdn_msg_id(context, msg_id, contact_id, smtp).await {
|
||||
Err(err) => {
|
||||
// If there is an error, for example there is no message corresponding to the msg_id in the
|
||||
// database, do not try to send this MDN again.
|
||||
warn!(
|
||||
context,
|
||||
"Error sending MDN for {msg_id}, removing it: {err:#}."
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.await?;
|
||||
Err(err)
|
||||
}
|
||||
Ok(false) => {
|
||||
bail!("Temporary error while sending an MDN");
|
||||
}
|
||||
Ok(true) => {
|
||||
// Successfully sent MDN.
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
// If there's a temporary error, pretend there are no more MDNs to send. It's unlikely that
|
||||
// other MDNs could be sent successfully in case of connectivity problems.
|
||||
res
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use deltachat_contact_tools::EmailAddress;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, ShowEmails};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::imap;
|
||||
use crate::message::MsgId;
|
||||
@@ -137,9 +137,9 @@ ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
|
||||
// the current ones are defined by chats.blocked=2
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;"
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);"
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;")
|
||||
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
|
||||
27,
|
||||
)
|
||||
@@ -267,7 +267,7 @@ CREATE INDEX msgs_index6 ON msgs (location_id);"#,
|
||||
// so, msg_id may or may not exist.
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);",
|
||||
CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
|
||||
.await?;
|
||||
if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() {
|
||||
@@ -839,11 +839,7 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
|
||||
if dbversion < 108 {
|
||||
let version = 108;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
sql.transaction(move |trans| {
|
||||
Sql::set_db_version_trans(trans, version)?;
|
||||
let id_max =
|
||||
@@ -936,6 +932,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 114 {
|
||||
sql.execute_migration("CREATE INDEX reactions_index1 ON reactions (msg_id)", 114)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -193,7 +193,18 @@ impl TestContextManager {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<KeyPair>,
|
||||
log_sink: LogSink,
|
||||
|
||||
/// Log sink if set.
|
||||
///
|
||||
/// If log sink is not set,
|
||||
/// a new one will be created and stored
|
||||
/// inside the test context when it is built.
|
||||
/// If log sink is provided by the caller,
|
||||
/// it will be subscribed to the test context,
|
||||
/// but not stored inside of it,
|
||||
/// so the caller should store the LogSink elsewhere to
|
||||
/// prevent it from being dropped immediately.
|
||||
log_sink: Option<LogSink>,
|
||||
}
|
||||
|
||||
impl TestContextBuilder {
|
||||
@@ -234,7 +245,7 @@ impl TestContextBuilder {
|
||||
/// sequence as they occurred rather than all messages from each context in a single
|
||||
/// block.
|
||||
pub fn with_log_sink(mut self, sink: LogSink) -> Self {
|
||||
self.log_sink = sink;
|
||||
self.log_sink = Some(sink);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -242,7 +253,7 @@ impl TestContextBuilder {
|
||||
pub async fn build(self) -> TestContext {
|
||||
let name = self.key_pair.as_ref().map(|key| key.addr.local.clone());
|
||||
|
||||
let test_context = TestContext::new_internal(name, Some(self.log_sink.clone())).await;
|
||||
let test_context = TestContext::new_internal(name, self.log_sink).await;
|
||||
|
||||
if let Some(key_pair) = self.key_pair {
|
||||
test_context
|
||||
|
||||
@@ -327,6 +327,12 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob:
|
||||
last_info_msg.is_none(),
|
||||
"{last_info_msg:?} shouldn't be there (or it's an unrelated info msg)"
|
||||
);
|
||||
|
||||
let sent = bob.send_text(*group, "hi").await;
|
||||
let msg = Message::load_from_db(bob, sent.sender_msg_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
src/tools.rs
59
src/tools.rs
@@ -294,31 +294,6 @@ pub(crate) fn create_outgoing_rfc724_mid() -> String {
|
||||
format!("Mr.{}.{}@localhost", create_id(), create_id())
|
||||
}
|
||||
|
||||
/// Extract the group id (grpid) from a message id (mid)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `mid` - A string that holds the message id. Leading/Trailing <>
|
||||
/// characters are automatically stripped.
|
||||
pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
|
||||
let mid = mid.trim_start_matches('<').trim_end_matches('>');
|
||||
|
||||
if mid.len() < 9 || !mid.starts_with("Gr.") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(mid_without_offset) = mid.get(3..) {
|
||||
if let Some(grpid_len) = mid_without_offset.find('.') {
|
||||
/* strict length comparison, the 'Gr.' magic is weak enough */
|
||||
if grpid_len == 11 || grpid_len == 16 {
|
||||
return Some(mid_without_offset.get(0..grpid_len).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// the returned suffix is lower-case
|
||||
pub fn get_filesuffix_lc(path_filename: &str) -> Option<String> {
|
||||
Path::new(path_filename)
|
||||
@@ -925,45 +900,11 @@ DKIM Results: Passed=true";
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_grpid_from_rfc724_mid() {
|
||||
// Should return None if we pass invalid mid
|
||||
let mid = "foobar";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, None);
|
||||
|
||||
// Should return None if grpid has a length which is not 11 or 16
|
||||
let mid = "Gr.12345678.morerandom@domain.de";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, None);
|
||||
|
||||
// Should return extracted grpid for grpid with length of 11
|
||||
let mid = "Gr.12345678901.morerandom@domain.de";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, Some("12345678901"));
|
||||
|
||||
// Should return extracted grpid for grpid with length of 11
|
||||
let mid = "Gr.1234567890123456.morerandom@domain.de";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, Some("1234567890123456"));
|
||||
|
||||
// Should return extracted grpid for grpid with length of 11
|
||||
let mid = "<Gr.12345678901.morerandom@domain.de>";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, Some("12345678901"));
|
||||
|
||||
// Should return extracted grpid for grpid with length of 11
|
||||
let mid = "<Gr.1234567890123456.morerandom@domain.de>";
|
||||
let grpid = extract_grpid_from_rfc724_mid(mid);
|
||||
assert_eq!(grpid, Some("1234567890123456"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_outgoing_rfc724_mid() {
|
||||
let mid = create_outgoing_rfc724_mid();
|
||||
assert!(mid.starts_with("Mr."));
|
||||
assert!(mid.ends_with("@localhost"));
|
||||
assert!(extract_grpid_from_rfc724_mid(mid.as_str()).is_none());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|
||||
@@ -25,6 +25,7 @@ Messenger functions | [Chat-over-Email](https://github.com/deltacha
|
||||
Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][])
|
||||
User and chat colors | [XEP-0392][]: Consistent Color Generation
|
||||
Send and receive system messages | Multipart/Report Media Type ([RFC 6522][])
|
||||
Send and receive contact files | vCard ([RFC 6350][])
|
||||
Return receipts | Message Disposition Notification (MDN, [RFC 8098][], [RFC 3503][]) using the Chat-Disposition-Notification-To header
|
||||
Locations | KML ([Open Geospatial Consortium](http://www.opengeospatial.org/standards/kml/), [Google Dev](https://developers.google.com/kml/))
|
||||
|
||||
@@ -53,6 +54,7 @@ Locations | KML ([Open Geospatial Consortium](http://www.
|
||||
[RFC 5321]: https://tools.ietf.org/html/rfc5321
|
||||
[RFC 5322]: https://tools.ietf.org/html/rfc5322
|
||||
[RFC 6154]: https://tools.ietf.org/html/rfc6154
|
||||
[RFC 6350]: https://tools.ietf.org/html/rfc6350
|
||||
[RFC 6522]: https://tools.ietf.org/html/rfc6522
|
||||
[RFC 6749]: https://tools.ietf.org/html/rfc6749
|
||||
[RFC 7162]: https://tools.ietf.org/html/rfc7162
|
||||
|
||||
Reference in New Issue
Block a user