mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
11 Commits
link2xt/te
...
v1.144.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486ea3a358 | ||
|
|
624ae86913 | ||
|
|
b47b96d5d6 | ||
|
|
f6b5c5d150 | ||
|
|
9cc65c615c | ||
|
|
d6845bd5e9 | ||
|
|
0b908db272 | ||
|
|
841ed43f11 | ||
|
|
60cd6f56be | ||
|
|
060fd55249 | ||
|
|
38c7f7300e |
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## [1.144.0] - 2024-09-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Make QR code type for proxy not specific to SOCKS5 ([#5980](https://github.com/deltachat/deltachat-core-rust/pull/5980)).
|
||||
|
||||
`DC_QR_SOCKS5_PROXY` is replaced with `DC_QR_PROXY`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Make resending OutPending messages possible ([#5817](https://github.com/deltachat/deltachat-core-rust/pull/5817)).
|
||||
- Don't SMTP-send messages to self-chat if BccSelf is disabled.
|
||||
- HTTP(S) tunneling.
|
||||
- Don't put displayname into From/To/Sender if it equals to address ([#5983](https://github.com/deltachat/deltachat-core-rust/pull/5983)).
|
||||
- Use IMAP APPEND command to upload sync messages ([#5845](https://github.com/deltachat/deltachat-core-rust/pull/5845)).
|
||||
- Generate 144-bit group IDs.
|
||||
- smtp: More verbose SMTP connection establishment errors.
|
||||
- Log unexpected message state when resending fails.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Save QR code token regardless of whether the group exists ([#5954](https://github.com/deltachat/deltachat-core-rust/pull/5954)).
|
||||
- Shorten message text in locally sent messages too ([#2281](https://github.com/deltachat/deltachat-core-rust/pull/2281)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- CONTRIBUTING.md: Document how to format SQL statements.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider database.
|
||||
- cargo: Update iroh to 0.25.
|
||||
- cargo: Update lazy_static to 1.5.0.
|
||||
- deps: Bump async-imap from 0.10.0 to 0.10.1.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not store deprecated `addr` and `is_default` into `keypairs`.
|
||||
- Remove `addr` from KeyPair.
|
||||
- Use `KeyPair::new()` in `create_keypair()`.
|
||||
|
||||
## [1.143.0] - 2024-09-12
|
||||
|
||||
### Features / Changes
|
||||
@@ -4856,3 +4897,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.142.11]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.10..v1.142.11
|
||||
[1.142.12]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.11..v1.142.12
|
||||
[1.143.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.12..v1.143.0
|
||||
[1.144.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.143.0..v1.144.0
|
||||
|
||||
319
Cargo.lock
generated
319
Cargo.lock
generated
@@ -199,9 +199,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.5.2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0"
|
||||
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
@@ -215,25 +215,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c"
|
||||
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.77",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -286,9 +286,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bbfed56b06fcd4d9c3255ba7ea5dc5dbc06ec25130fd7d6698d44ee13cd89f0"
|
||||
checksum = "2162818f7b394e342a6591864bfc960824ed13e3f6609fee0d19e770ebcd99e1"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"base64 0.21.7",
|
||||
@@ -327,6 +327,17 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.9.1"
|
||||
@@ -742,6 +753,12 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfb-mode"
|
||||
version = "0.8.2"
|
||||
@@ -913,6 +930,16 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -1240,7 +1267,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1336,7 +1363,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1361,7 +1388,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1376,7 +1403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1405,7 +1432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1435,9 +1462,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "8.2.0"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e"
|
||||
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
@@ -2094,7 +2121,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2451,10 +2478,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.0"
|
||||
version = "0.25.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf"
|
||||
checksum = "8270a1857fb962b9914aafd46a89a187a4e63d0eb4190c327e7c7b8256a2d055"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
@@ -2462,11 +2490,12 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna 0.4.0",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2475,9 +2504,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243"
|
||||
checksum = "46c110355b5703070d9e29c344d79818a7cde3de9c27fc35750defea6074b0ad"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -2705,12 +2734,12 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls 0.23.10",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 0.26.1",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2771,16 +2800,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@@ -2894,9 +2913,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "iroh-base"
|
||||
version = "0.23.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8d60492b6099a5e94b674d0f4c564b3fa63a211836a090bc34c14d422721c19"
|
||||
checksum = "545424889bce87e44fd08dac9e6889635630fdbdaaa858a3c5a5f0adbb8d3779"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"anyhow",
|
||||
@@ -2935,9 +2954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-gossip"
|
||||
version = "0.23.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11551a79338d0d3aac1cff957a8801a2c3ffccce59fa1f3d60ca183aa785edab"
|
||||
checksum = "6a5489a563e407fb2be654e950536da4230e46642111f12b16d7013228164aae"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -2963,9 +2982,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-metrics"
|
||||
version = "0.23.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0be681eb052594a43afa8d2d2cc093361917c7348c779df3fe92a7169bd2f9c5"
|
||||
checksum = "9fb6ab80f7cccde80be07a643308e1ff925d5be91721ec65ba96b261a0bcb5e5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"erased_set",
|
||||
@@ -2984,9 +3003,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-net"
|
||||
version = "0.23.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b18cd649ce3a342d8480038abee8170e483bc3bec8f111bf57ca9679c5075ce"
|
||||
checksum = "4a099a60478cb9319153756a9cf2ff41aa03d18403690a91e60f049ad467b057"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backoff",
|
||||
@@ -3027,13 +3046,12 @@ dependencies = [
|
||||
"pkarr",
|
||||
"postcard",
|
||||
"rand 0.8.5",
|
||||
"rand_core 0.6.4",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rtnetlink",
|
||||
"rustls 0.21.11",
|
||||
"rustls-webpki 0.101.7",
|
||||
"rustls",
|
||||
"rustls-webpki",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
@@ -3043,7 +3061,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time 0.3.36",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite-wasm",
|
||||
"tokio-util",
|
||||
@@ -3051,7 +3069,7 @@ dependencies = [
|
||||
"tungstenite",
|
||||
"url",
|
||||
"watchable",
|
||||
"webpki-roots 0.25.4",
|
||||
"webpki-roots",
|
||||
"windows 0.51.1",
|
||||
"wmi",
|
||||
"x509-parser",
|
||||
@@ -3060,16 +3078,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn"
|
||||
version = "0.10.5"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "906875956feb75d3d41d708ddaffeb11fdb10cd05f23efbcb17600037e411779"
|
||||
checksum = "4fd590a39a14cfc168efa4d894de5039d65641e62d8da4a80733018ababe3c33"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"iroh-quinn-proto",
|
||||
"iroh-quinn-udp",
|
||||
"pin-project-lite",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustls 0.21.11",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3077,16 +3096,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn-proto"
|
||||
version = "0.10.8"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6bf92478805e67f2320459285496e1137edf5171411001a0d4d85f9bbafb792"
|
||||
checksum = "5fd0538ff12efe3d61ea1deda2d7913f4270873a519d43e6995c6e87a1558538"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustls 0.21.11",
|
||||
"rustls-native-certs",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
@@ -3095,15 +3114,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iroh-quinn-udp"
|
||||
version = "0.4.2"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc7915b3a31f08ee0bc02f73f4d61a5d5be146a1081ef7f70622a11627fd314"
|
||||
checksum = "d0619b59471fdd393ac8a6c047f640171119c1c8b41f7d2927db91776dcdbc5f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3138,6 +3157,26 @@ version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.69"
|
||||
@@ -3181,11 +3220,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin 0.5.2",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3591,11 +3630,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
@@ -3719,9 +3757,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.6.1"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff"
|
||||
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
@@ -4459,7 +4497,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustls 0.23.10",
|
||||
"rustls",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4475,7 +4513,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls 0.23.10",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
@@ -4764,21 +4802,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.10",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.0",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.1",
|
||||
"webpki-roots",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
@@ -4812,7 +4850,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.12",
|
||||
"libc",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -4953,18 +4991,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.10"
|
||||
@@ -4975,32 +5001,24 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.4",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.3"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.2"
|
||||
@@ -5018,15 +5036,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-roots",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.4"
|
||||
@@ -5139,16 +5174,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -5165,22 +5190,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"num-bigint",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.9.1"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
|
||||
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -5384,7 +5410,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"shadowsocks-crypto",
|
||||
"socket2",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tfo",
|
||||
@@ -5501,12 +5527,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -5728,14 +5748,13 @@ checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c"
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.6"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
|
||||
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"unicode-xid",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5959,23 +5978,13 @@ dependencies = [
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls 0.21.11",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.10",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
@@ -6055,13 +6064,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@@ -6354,10 +6365,10 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.10",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots 0.26.1",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6367,7 +6378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 0.5.0",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -6549,12 +6560,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.1"
|
||||
@@ -6894,9 +6899,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da"
|
||||
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.0", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.10.1", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
@@ -57,14 +57,14 @@ fd-lock = "4"
|
||||
futures = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
hickory-resolver = "=0.25.0-alpha.2"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.7"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-net = { version = "0.23.0", default-features = false }
|
||||
iroh-gossip = { version = "0.23.0", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.25.0", default-features = false }
|
||||
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -2501,7 +2501,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_SOCKS5_PROXY 270 // text1=host, text2=port
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
|
||||
@@ -34,44 +34,41 @@ pub enum Meaning {
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
pub fn get_text1(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||
Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
|
||||
Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
|
||||
Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Socks5Proxy { host, .. } => Some(host),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::Login { address, .. } => Some(address),
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))),
|
||||
_ => None,
|
||||
},
|
||||
Self::Qr(_) => None,
|
||||
Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -107,7 +104,7 @@ impl Lot {
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
@@ -133,7 +130,7 @@ impl Lot {
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Socks5Proxy { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
@@ -188,8 +185,8 @@ pub enum LotState {
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=host, text2=port
|
||||
QrSocks5Proxy = 270,
|
||||
/// text1=address, text2=protocol
|
||||
QrProxy = 271,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -41,11 +41,10 @@ pub enum QrObject {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Socks5Proxy {
|
||||
Proxy {
|
||||
url: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
user: Option<String>,
|
||||
pass: Option<String>,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
@@ -152,17 +151,7 @@ impl From<Qr> for QrObject {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
} => QrObject::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
},
|
||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.143.0"
|
||||
"version": "1.144.0"
|
||||
}
|
||||
|
||||
@@ -86,10 +86,7 @@ describe("online tests", function () {
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
|
||||
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
|
||||
const { chatId: chatIdOnAccountB } = await eventPromise;
|
||||
@@ -119,10 +116,7 @@ describe("online tests", function () {
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||
// wait for message from A
|
||||
console.log("wait for message from A");
|
||||
@@ -143,10 +137,7 @@ describe("online tests", function () {
|
||||
);
|
||||
expect(message.text).equal("Hello2");
|
||||
// Send message back from B to A
|
||||
const eventPromise2 = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId1),
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
]);
|
||||
const eventPromise2 = waitForEvent(dc, "IncomingMsg", accountId1);
|
||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||
// Check if answer arrives at A and if it is encrypted
|
||||
await eventPromise2;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.143.0"
|
||||
"version": "1.144.0"
|
||||
}
|
||||
|
||||
14
deny.toml
14
deny.toml
@@ -19,14 +19,10 @@ ignore = [
|
||||
# when upgrading.
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "asn1-rs-derive", version = "0.4.0" },
|
||||
{ name = "asn1-rs-impl", version = "0.1.0" },
|
||||
{ name = "asn1-rs", version = "0.5.2" },
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "der-parser", version = "8.2.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ name = "fastrand", version = "1.9.0" },
|
||||
@@ -36,9 +32,7 @@ skip = [
|
||||
{ name = "http-body", version = "0.4.6" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hyper", version = "0.14.28" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "oid-registry", version = "0.6.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -46,17 +40,10 @@ skip = [
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustls-pemfile", version = "1.0.4" },
|
||||
{ name = "rustls", version = "0.21.11" },
|
||||
{ name = "rustls-webpki", version = "0.101.7" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "synstructure", version = "0.12.6" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-rustls", version = "0.24.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" },
|
||||
@@ -69,7 +56,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
{ name = "x509-parser", version = "<0.16.0" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -134,9 +134,9 @@ module.exports = {
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_LOGIN: 520,
|
||||
DC_QR_PROXY: 271,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_SOCKS5_PROXY: 270,
|
||||
DC_QR_TEXT: 330,
|
||||
DC_QR_URL: 332,
|
||||
DC_QR_WEBRTC_INSTANCE: 260,
|
||||
|
||||
@@ -134,9 +134,9 @@ export enum C {
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_LOGIN = 520,
|
||||
DC_QR_PROXY = 271,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_SOCKS5_PROXY = 270,
|
||||
DC_QR_TEXT = 330,
|
||||
DC_QR_URL = 332,
|
||||
DC_QR_WEBRTC_INSTANCE = 260,
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.143.0"
|
||||
"version": "1.144.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.143.0"
|
||||
version = "1.144.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -488,10 +488,18 @@ def test_move_sync_msgs(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
# Sync messages may also be sent during the configuration.
|
||||
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-09-12
|
||||
2024-09-21
|
||||
61
src/chat.rs
61
src/chat.rs
@@ -2245,7 +2245,7 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat { id, action })
|
||||
.await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2906,10 +2906,9 @@ async fn prepare_send_msg(
|
||||
create_send_msg_jobs(context, msg).await
|
||||
}
|
||||
|
||||
/// Constructs jobs for sending a message and inserts them into the `smtp` table.
|
||||
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
||||
///
|
||||
/// Returns row ids if jobs were created or an empty `Vec` otherwise, e.g. when sending to a
|
||||
/// group with only self and no BCC-to-self configured.
|
||||
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
@@ -3003,12 +3002,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {err:#}.");
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
@@ -3025,19 +3018,30 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
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) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
let row_id = t.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
),
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
t.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
t.execute(
|
||||
"INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
|
||||
(&rendered_msg.message, msg.id),
|
||||
)?;
|
||||
} else {
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
let row_id = t.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
),
|
||||
)?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
}
|
||||
Ok(row_ids)
|
||||
};
|
||||
@@ -3793,7 +3797,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
{
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
@@ -6288,11 +6292,10 @@ mod tests {
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
|
||||
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
|
||||
assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
@@ -6309,7 +6312,7 @@ mod tests {
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
|
||||
@@ -98,7 +98,8 @@ pub enum Config {
|
||||
|
||||
/// Proxy URL.
|
||||
///
|
||||
/// Supported URLs schemes are `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
|
||||
/// Supported URLs schemes are `http://` (HTTP), `https://` (HTTPS),
|
||||
/// `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
|
||||
///
|
||||
/// May contain multiple URLs separated by newline, in which case the first one is used.
|
||||
ProxyUrl,
|
||||
@@ -588,6 +589,12 @@ impl Context {
|
||||
&& !self.get_config_bool(Config::Bot).await?)
|
||||
}
|
||||
|
||||
/// Returns whether sync messages should be uploaded to the mvbox.
|
||||
pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns whether MDNs should be requested.
|
||||
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
|
||||
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
|
||||
@@ -791,7 +798,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1174,7 +1181,7 @@ mod tests {
|
||||
let status = "Synced via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_msg().await;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
let status1 = "Synced via sync message";
|
||||
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
@@ -1198,7 +1205,7 @@ mod tests {
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_msg().await;
|
||||
alice0.pop_sent_sync_msg().await;
|
||||
let file = alice1.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
|
||||
47
src/imap.rs
47
src/imap.rs
@@ -32,6 +32,7 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{
|
||||
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
};
|
||||
@@ -1043,6 +1044,52 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uploads sync messages from the `imap_send` table with `\Seen` flag set.
|
||||
pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
context.send_sync_msg().await?;
|
||||
while let Some((id, mime, msg_id, attempts)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
|
||||
(),
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let mime: String = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
let attempts: i64 = row.get(3)?;
|
||||
Ok((id, mime, msg_id, attempts))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to SELECT from imap_send")?
|
||||
{
|
||||
let res = self
|
||||
.append(folder, Some("(\\Seen)"), None, mime)
|
||||
.await
|
||||
.with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
|
||||
.log_err(context);
|
||||
if res.is_ok() {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
const MAX_ATTEMPTS: i64 = 2;
|
||||
if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap_send WHERE id=?", (id,))
|
||||
.await
|
||||
.context("Failed to delete from imap_send")?;
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
|
||||
.await
|
||||
.context("Failed to update imap_send.attempts")?;
|
||||
res?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
|
||||
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
|
||||
let rows = context
|
||||
|
||||
@@ -97,7 +97,7 @@ impl BackupProvider {
|
||||
let endpoint = Endpoint::builder()
|
||||
.alpns(vec![BACKUP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind(0)
|
||||
.bind()
|
||||
.await?;
|
||||
let node_addr = endpoint.node_addr().await?;
|
||||
|
||||
@@ -220,6 +220,13 @@ impl BackupProvider {
|
||||
|
||||
conn = endpoint.accept() => {
|
||||
if let Some(conn) = conn {
|
||||
let conn = match conn.accept() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to accept iroh connection: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Got a new in-progress connection.
|
||||
let context = context.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -274,7 +281,7 @@ pub async fn get_backup2(
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
|
||||
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind(0).await?;
|
||||
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind().await?;
|
||||
|
||||
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
|
||||
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
|
||||
@@ -296,9 +303,13 @@ pub async fn get_backup2(
|
||||
// Send an acknowledgement, but ignore the errors.
|
||||
// We have imported backup successfully already.
|
||||
send_stream.write_all(b".").await.ok();
|
||||
send_stream.finish().await.ok();
|
||||
send_stream.finish().ok();
|
||||
info!(context, "Sent backup reception acknowledgment.");
|
||||
|
||||
// Wait for the peer to acknowledge reception of the acknowledgement
|
||||
// before closing the connection.
|
||||
_ = send_stream.stopped().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,8 @@ pub struct RenderedEmail {
|
||||
pub is_gossiped: bool,
|
||||
pub last_added_location_id: Option<u32>,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email
|
||||
/// and must be deleted once the message is actually queued for sending
|
||||
/// (deletion must be done by `delete_sync_ids()`).
|
||||
/// If the rendered email is not queued for sending, the IDs must not be deleted.
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
|
||||
/// from `multi_device_sync` once the message is actually queued for sending.
|
||||
pub sync_ids_to_delete: Option<String>,
|
||||
|
||||
/// Message ID (Message in the sense of Email)
|
||||
@@ -117,6 +115,13 @@ pub struct RenderedEmail {
|
||||
pub subject: String,
|
||||
}
|
||||
|
||||
fn new_address_with_name(name: &str, address: String) -> Address {
|
||||
match name == address {
|
||||
true => Address::new_mailbox(address),
|
||||
false => Address::new_mailbox_with_name(name.to_string(), address),
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeFactory {
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
@@ -474,10 +479,7 @@ impl MimeFactory {
|
||||
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
|
||||
let mut headers = Vec::<Header>::new();
|
||||
|
||||
let from = Address::new_mailbox_with_name(
|
||||
self.from_displayname.to_string(),
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
|
||||
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast,
|
||||
@@ -512,10 +514,7 @@ impl MimeFactory {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
to.push(new_address_with_name(name, addr.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,8 +529,7 @@ impl MimeFactory {
|
||||
headers.push(from_header.clone());
|
||||
|
||||
if let Some(sender_displayname) = &self.sender_displayname {
|
||||
let sender =
|
||||
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
||||
let sender = new_address_with_name(sender_displayname, self.from_addr.clone());
|
||||
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
}
|
||||
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
|
||||
@@ -1676,10 +1674,7 @@ mod tests {
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
|
||||
|
||||
let s = format!(
|
||||
"{}",
|
||||
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
|
||||
);
|
||||
let s = format!("{}", new_address_with_name(display_name, addr.to_string()));
|
||||
|
||||
println!("{s}");
|
||||
|
||||
@@ -1696,15 +1691,19 @@ mod tests {
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
|
||||
|
||||
let s = format!(
|
||||
"{}",
|
||||
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
|
||||
);
|
||||
let s = format!("{}", new_address_with_name(display_name, addr.to_string()));
|
||||
|
||||
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
|
||||
assert_eq!(s, "a space <x@y.org>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address_duplicated_as_name() {
|
||||
let addr = "x@y.org";
|
||||
let s = format!("{}", new_address_with_name(addr, addr.to_string()));
|
||||
assert_eq!(s, "<x@y.org>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_rfc724_mid() {
|
||||
assert_eq!(
|
||||
@@ -2246,7 +2245,7 @@ mod tests {
|
||||
if name.is_empty() {
|
||||
Address::new_mailbox(addr.to_string())
|
||||
} else {
|
||||
Address::new_mailbox_with_name(name.to_string(), addr.to_string())
|
||||
new_address_with_name(name, addr.to_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
290
src/net/proxy.rs
290
src/net/proxy.rs
@@ -1,25 +1,28 @@
|
||||
//! # Proxy support.
|
||||
//!
|
||||
//! Delta Chat supports SOCKS5 and Shadowsocks protocols.
|
||||
//! Delta Chat supports HTTP(S) CONNECT, SOCKS5 and Shadowsocks protocols.
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use base64::Engine;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use fast_socks5::util::target_addr::ToTargetAddr;
|
||||
use fast_socks5::AuthenticationMethod;
|
||||
use fast_socks5::Socks5Command;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use pin_project::pin_project;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::{connect_tcp, wrap_tls};
|
||||
use crate::sql::Sql;
|
||||
|
||||
/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
|
||||
@@ -94,6 +97,51 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HttpConfig {
|
||||
/// HTTP proxy host.
|
||||
pub host: String,
|
||||
|
||||
/// HTTP proxy port.
|
||||
pub port: u16,
|
||||
|
||||
/// Username and password for basic authentication.
|
||||
///
|
||||
/// If set, `Proxy-Authorization` header is sent.
|
||||
pub user_password: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl HttpConfig {
|
||||
fn from_url(url: Url) -> Result<Self> {
|
||||
let host = url
|
||||
.host_str()
|
||||
.context("HTTP proxy URL has no host")?
|
||||
.to_string();
|
||||
let port = url
|
||||
.port_or_known_default()
|
||||
.context("HTTP(S) URLs are guaranteed to return Some port")?;
|
||||
let user_password = if let Some(password) = url.password() {
|
||||
let username = percent_encoding::percent_decode_str(url.username())
|
||||
.decode_utf8()
|
||||
.context("HTTP(S) proxy username is not a valid UTF-8")?
|
||||
.to_string();
|
||||
let password = percent_encoding::percent_decode_str(password)
|
||||
.decode_utf8()
|
||||
.context("HTTP(S) proxy password is not a valid UTF-8")?
|
||||
.to_string();
|
||||
Some((username, password))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let http_config = HttpConfig {
|
||||
host,
|
||||
port,
|
||||
user_password,
|
||||
};
|
||||
Ok(http_config)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Socks5Config {
|
||||
pub host: String,
|
||||
@@ -135,16 +183,107 @@ impl Socks5Config {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProxyConfig {
|
||||
// HTTP proxy.
|
||||
Http(HttpConfig),
|
||||
|
||||
// HTTPS proxy.
|
||||
Https(HttpConfig),
|
||||
|
||||
// SOCKS5 proxy.
|
||||
Socks5(Socks5Config),
|
||||
|
||||
// Shadowsocks proxy.
|
||||
Shadowsocks(ShadowsocksConfig),
|
||||
}
|
||||
|
||||
/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
|
||||
fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
|
||||
// According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
|
||||
// clients MUST send `Host:` header in HTTP/1.1 requests,
|
||||
// so repeat the host there.
|
||||
let mut res = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n");
|
||||
if let Some((username, password)) = auth {
|
||||
res += "Proxy-Authorization: Basic ";
|
||||
res += &base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
|
||||
res += "\r\n";
|
||||
}
|
||||
res += "\r\n";
|
||||
res
|
||||
}
|
||||
|
||||
/// Sends HTTP/1.1 `CONNECT` request over given connection
|
||||
/// to establish an HTTP tunnel.
|
||||
///
|
||||
/// Returns the same connection back so actual data can be tunneled over it.
|
||||
async fn http_tunnel<T>(mut conn: T, host: &str, port: u16, auth: Option<(&str, &str)>) -> Result<T>
|
||||
where
|
||||
T: AsyncReadExt + AsyncWriteExt + Unpin,
|
||||
{
|
||||
// Send HTTP/1.1 CONNECT request.
|
||||
let request = http_connect_request(host, port, auth);
|
||||
conn.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut buffer = BytesMut::with_capacity(4096);
|
||||
|
||||
let res = loop {
|
||||
if !buffer.has_remaining_mut() {
|
||||
bail!("CONNECT response exceeded buffer size");
|
||||
}
|
||||
let n = conn.read_buf(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
bail!("Unexpected end of CONNECT response");
|
||||
}
|
||||
|
||||
let res = &buffer[..];
|
||||
if res.ends_with(b"\r\n\r\n") {
|
||||
// End of response is not reached, read more.
|
||||
break res;
|
||||
}
|
||||
};
|
||||
|
||||
// Normally response looks like
|
||||
// `HTTP/1.1 200 Connection established\r\n\r\n`.
|
||||
if !res.starts_with(b"HTTP/") {
|
||||
bail!("Unexpected HTTP CONNECT response: {res:?}");
|
||||
}
|
||||
|
||||
// HTTP-version followed by space has fixed length
|
||||
// according to RFC 7230:
|
||||
// <https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2>
|
||||
//
|
||||
// Normally status line starts with `HTTP/1.1 `.
|
||||
// We only care about 3-digit status code.
|
||||
let status_code = res
|
||||
.get(9..12)
|
||||
.context("HTTP status line does not contain a status code")?;
|
||||
|
||||
// Interpert status code according to
|
||||
// <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
|
||||
if status_code == b"407" {
|
||||
Err(format_err!("Proxy Authentication Required"))
|
||||
} else if status_code.starts_with(b"2") {
|
||||
// Success.
|
||||
Ok(conn)
|
||||
} else {
|
||||
Err(format_err!(
|
||||
"Failed to establish HTTP CONNECT tunnel: {res:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyConfig {
|
||||
/// Creates a new proxy configuration by parsing given proxy URL.
|
||||
fn from_url(url: &str) -> Result<Self> {
|
||||
let url = url::Url::parse(url).context("Cannot parse proxy URL")?;
|
||||
let url = Url::parse(url).context("Cannot parse proxy URL")?;
|
||||
match url.scheme() {
|
||||
"http" => {
|
||||
let http_config = HttpConfig::from_url(url)?;
|
||||
Ok(Self::Http(http_config))
|
||||
}
|
||||
"https" => {
|
||||
let https_config = HttpConfig::from_url(url)?;
|
||||
Ok(Self::Https(https_config))
|
||||
}
|
||||
"ss" => {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
|
||||
let shadowsocks_config = ShadowsocksConfig { server_config };
|
||||
@@ -274,6 +413,42 @@ impl ProxyConfig {
|
||||
load_dns_cache: bool,
|
||||
) -> Result<Box<dyn SessionStream>> {
|
||||
match self {
|
||||
ProxyConfig::Http(http_config) => {
|
||||
let load_cache = false;
|
||||
let tcp_stream = crate::net::connect_tcp(
|
||||
context,
|
||||
&http_config.host,
|
||||
http_config.port,
|
||||
load_cache,
|
||||
)
|
||||
.await?;
|
||||
let auth = if let Some((username, password)) = &http_config.user_password {
|
||||
Some((username.as_str(), password.as_str()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tunnel_stream = http_tunnel(tcp_stream, target_host, target_port, auth).await?;
|
||||
Ok(Box::new(tunnel_stream))
|
||||
}
|
||||
ProxyConfig::Https(https_config) => {
|
||||
let load_cache = true;
|
||||
let strict_tls = true;
|
||||
let tcp_stream = crate::net::connect_tcp(
|
||||
context,
|
||||
&https_config.host,
|
||||
https_config.port,
|
||||
load_cache,
|
||||
)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, &https_config.host, &[], tcp_stream).await?;
|
||||
let auth = if let Some((username, password)) = &https_config.user_password {
|
||||
Some((username.as_str(), password.as_str()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tunnel_stream = http_tunnel(tls_stream, target_host, target_port, auth).await?;
|
||||
Ok(Box::new(tunnel_stream))
|
||||
}
|
||||
ProxyConfig::Socks5(socks5_config) => {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, target_host, target_port, load_dns_cache)
|
||||
@@ -363,6 +538,111 @@ mod tests {
|
||||
user_password: Some(("foo".to_string(), "bar".to_string()))
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:80").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 1080,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:1080").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Socks5(Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 1080,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_url() {
|
||||
let proxy_config = ProxyConfig::from_url("http://127.0.0.1").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Http(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Http(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Http(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 443,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_https_url() {
|
||||
let proxy_config = ProxyConfig::from_url("https://127.0.0.1").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Https(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 443,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("https://127.0.0.1:80").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Https(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
|
||||
let proxy_config = ProxyConfig::from_url("https://127.0.0.1:443").unwrap();
|
||||
assert_eq!(
|
||||
proxy_config,
|
||||
ProxyConfig::Https(HttpConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 443,
|
||||
user_password: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_connect_request() {
|
||||
assert_eq!(http_connect_request("example.org", 143, Some(("aladdin", "opensesame"))), "CONNECT example.org:143 HTTP/1.1\r\nHost: example.org:143\r\nProxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\r\n\r\n");
|
||||
assert_eq!(
|
||||
http_connect_request("example.net", 587, None),
|
||||
"CONNECT example.net:587 HTTP/1.1\r\nHost: example.net:587\r\n\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -253,7 +253,7 @@ impl Context {
|
||||
.secret_key(secret_key)
|
||||
.alpns(vec![GOSSIP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind(0)
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
// create gossip
|
||||
@@ -265,7 +265,6 @@ impl Context {
|
||||
|
||||
// Shuts down on deltachat shutdown
|
||||
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
|
||||
tokio::spawn(gossip_direct_address_loop(endpoint.clone(), gossip.clone()));
|
||||
|
||||
Ok(Iroh {
|
||||
endpoint,
|
||||
@@ -285,15 +284,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop to update direct addresses of the gossip.
|
||||
async fn gossip_direct_address_loop(endpoint: Endpoint, gossip: Gossip) -> Result<()> {
|
||||
let mut stream = endpoint.direct_addresses();
|
||||
while let Some(addrs) = stream.next().await {
|
||||
gossip.update_direct_addresses(&addrs)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cache a peers [NodeId] for one topic.
|
||||
pub(crate) async fn iroh_add_peer_for_topic(
|
||||
ctx: &Context,
|
||||
@@ -442,6 +432,13 @@ pub(crate) async fn create_iroh_header(
|
||||
|
||||
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
|
||||
while let Some(conn) = endpoint.accept().await {
|
||||
let conn = match conn.accept() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to accept iroh connection: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
info!(context, "IROH_REALTIME: accepting iroh connection");
|
||||
let gossip = gossip.clone();
|
||||
let context = context.clone();
|
||||
|
||||
36
src/pgp.rs
36
src/pgp.rs
@@ -405,7 +405,7 @@ mod tests {
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, bob_keypair, TestContextManager};
|
||||
use crate::test_utils::{alice_keypair, bob_keypair};
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
@@ -592,38 +592,4 @@ mod tests {
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_rotate_encryption_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv(alice, bob, "Hi!").await;
|
||||
|
||||
// TODO: alice changes encryption key
|
||||
let mut alice_keyring = crate::key::load_self_secret_keyring(alice).await?;
|
||||
let mut alice_key = alice_keyring.pop().unwrap();
|
||||
|
||||
// There should be no public subkeys.
|
||||
assert!(alice_key.public_subkeys.is_empty());
|
||||
|
||||
// There is exactly one secret subkey
|
||||
// that we are going to replace.
|
||||
assert_eq!(alice_key.secret_subkeys.len(), 1);
|
||||
alice_key.secret_subkeys.clear();
|
||||
|
||||
let new_subkey = SubkeyParamsBuilder::default()
|
||||
.key_type(PgpKeyType::ECDH(ECCCurve::Curve25519))
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
.build()
|
||||
.context("Failed to build subkey parameters")?;
|
||||
|
||||
let new_keypair = KeyPair::new(alice_key)?;
|
||||
|
||||
tcm.send_recv(alice, bob, "Hi again, with a new key!").await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
271
src/qr.rs
271
src/qr.rs
@@ -36,8 +36,8 @@ const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
||||
const SMTP_SCHEME: &str = "SMTP:";
|
||||
const HTTP_SCHEME: &str = "http://";
|
||||
const HTTPS_SCHEME: &str = "https://";
|
||||
const SHADOWSOCKS_SCHEME: &str = "ss://";
|
||||
|
||||
/// Backup transfer based on iroh-net.
|
||||
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
|
||||
@@ -127,19 +127,26 @@ pub enum Qr {
|
||||
instance_pattern: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to add or use the given SOCKS5 proxy
|
||||
Socks5Proxy {
|
||||
/// SOCKS5 server
|
||||
/// Ask the user if they want to use the given proxy.
|
||||
///
|
||||
/// Note that HTTP(S) URLs without a path
|
||||
/// and query parameters are treated as HTTP(S) proxy URL.
|
||||
/// UI may want to still offer to open the URL
|
||||
/// in the browser if QR code contents
|
||||
/// starts with `http://` or `https://`
|
||||
/// and the QR code was not scanned from
|
||||
/// the proxy configuration screen.
|
||||
Proxy {
|
||||
/// Proxy URL.
|
||||
///
|
||||
/// This is the URL that is going to be added.
|
||||
url: String,
|
||||
|
||||
/// Host extracted from the URL to display in the UI.
|
||||
host: String,
|
||||
|
||||
/// SOCKS5 port
|
||||
/// Port extracted from the URL to display in the UI.
|
||||
port: u16,
|
||||
|
||||
/// SOCKS5 user
|
||||
user: Option<String>,
|
||||
|
||||
/// SOCKS5 password
|
||||
pass: Option<String>,
|
||||
},
|
||||
|
||||
/// Contact address is scanned.
|
||||
@@ -279,6 +286,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
decode_webrtc_instance(context, qr)?
|
||||
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
|
||||
decode_tg_socks_proxy(context, qr)?
|
||||
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
|
||||
decode_shadowsocks_proxy(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
|
||||
decode_backup2(qr)?
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
@@ -289,9 +298,44 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
decode_matmsg(context, qr).await?
|
||||
} else if qr.starts_with(VCARD_SCHEME) {
|
||||
decode_vcard(context, qr).await?
|
||||
} else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) {
|
||||
Qr::Url {
|
||||
url: qr.to_string(),
|
||||
} else if let Ok(url) = url::Url::parse(qr) {
|
||||
match url.scheme() {
|
||||
"socks5" => Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
host: url.host_str().context("URL has no host")?.to_string(),
|
||||
port: url.port().unwrap_or(DEFAULT_SOCKS_PORT),
|
||||
},
|
||||
"http" | "https" => {
|
||||
// Parsing with a non-standard scheme
|
||||
// is a hack to work around the `url` crate bug
|
||||
// <https://github.com/servo/rust-url/issues/957>.
|
||||
let url = if let Some(rest) = qr.strip_prefix("http://") {
|
||||
url::Url::parse(&format!("foobarbaz://{rest}"))?
|
||||
} else if let Some(rest) = qr.strip_prefix("https://") {
|
||||
url::Url::parse(&format!("foobarbaz://{rest}"))?
|
||||
} else {
|
||||
// Should not happen.
|
||||
url
|
||||
};
|
||||
|
||||
if url.port().is_none() | (url.path() != "") | url.query().is_some() {
|
||||
// URL without a port, with a path or query cannot be a proxy URL.
|
||||
Qr::Url {
|
||||
url: qr.to_string(),
|
||||
}
|
||||
} else {
|
||||
Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
host: url.host_str().context("URL has no host")?.to_string(),
|
||||
port: url
|
||||
.port_or_known_default()
|
||||
.context("HTTP(S) URLs are guaranteed to return Some port")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Qr::Url {
|
||||
url: qr.to_string(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Qr::Text {
|
||||
@@ -558,16 +602,35 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(host) = host {
|
||||
Ok(Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
})
|
||||
} else {
|
||||
let Some(host) = host else {
|
||||
bail!("Bad t.me/socks url: {:?}", url);
|
||||
}
|
||||
};
|
||||
|
||||
let mut url = "socks5://".to_string();
|
||||
if let Some(pass) = pass {
|
||||
url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
url += ":";
|
||||
url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
url += "@";
|
||||
};
|
||||
url += &host;
|
||||
url += ":";
|
||||
url += &port.to_string();
|
||||
|
||||
Ok(Qr::Proxy { url, host, port })
|
||||
}
|
||||
|
||||
/// Decodes `ss://` URLs for Shadowsocks proxies.
|
||||
fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
|
||||
let addr = server_config.addr();
|
||||
let host = addr.host().to_string();
|
||||
let port = addr.port();
|
||||
Ok(Qr::Proxy {
|
||||
url: qr.to_string(),
|
||||
host,
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
|
||||
@@ -655,33 +718,16 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
|
||||
.await?;
|
||||
}
|
||||
Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
} => {
|
||||
let mut proxy_url = "socks5://".to_string();
|
||||
if let Some(pass) = pass {
|
||||
proxy_url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC)
|
||||
.to_string();
|
||||
proxy_url += ":";
|
||||
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
|
||||
proxy_url += "@";
|
||||
};
|
||||
proxy_url += &host;
|
||||
proxy_url += ":";
|
||||
proxy_url += &port.to_string();
|
||||
|
||||
Qr::Proxy { url, .. } => {
|
||||
let old_proxy_url_value = context
|
||||
.get_config(Config::ProxyUrl)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let proxy_urls: Vec<&str> = std::iter::once(proxy_url.as_str())
|
||||
let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
|
||||
.chain(
|
||||
old_proxy_url_value
|
||||
.split('\n')
|
||||
.filter(|s| !s.is_empty() && *s != proxy_url),
|
||||
.filter(|s| !s.is_empty() && *s != url),
|
||||
)
|
||||
.collect();
|
||||
context
|
||||
@@ -719,7 +765,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
|
||||
token::save(context, token::Namespace::Auth, None, &authcode).await?;
|
||||
context.sync_qr_code_tokens(None).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
invitenumber,
|
||||
@@ -736,7 +782,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
.await?;
|
||||
token::save(context, token::Namespace::Auth, Some(&grpid), &authcode).await?;
|
||||
context.sync_qr_code_tokens(Some(&grpid)).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
Qr::Login { address, options } => {
|
||||
configure_from_login_qr(context, &address, options).await?
|
||||
@@ -916,11 +962,38 @@ mod tests {
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "http://www.hello.com:80".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 80
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com".to_string()
|
||||
url: "http://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -931,11 +1004,38 @@ mod tests {
|
||||
async fn test_decode_https() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "https://www.hello.com:443".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 443
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com".to_string()
|
||||
url: "https://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1523,33 +1623,30 @@ mod tests {
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
Qr::Proxy {
|
||||
url: "socks5://84.53.239.95:4145".to_string(),
|
||||
host: "84.53.239.95".to_string(),
|
||||
port: 4145,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.bar:123".to_string(),
|
||||
host: "foo.bar".to_string(),
|
||||
port: 123,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.baz:1080".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 1080,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1560,11 +1657,10 @@ mod tests {
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
Qr::Proxy {
|
||||
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 12345,
|
||||
user: Some("ada".to_string()),
|
||||
pass: Some("ms!/$".to_string()),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1612,10 +1708,6 @@ mod tests {
|
||||
assert!(res.is_err());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
|
||||
assert!(res.is_err());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
@@ -1635,7 +1727,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_socks5_proxy_config_from_qr() -> Result<()> {
|
||||
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
||||
@@ -1682,6 +1774,57 @@ mod tests {
|
||||
)
|
||||
);
|
||||
|
||||
set_config_from_qr(
|
||||
&t,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_shadowsocks() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
|
||||
host: "192.168.100.1".to_string(),
|
||||
port: 8888,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_socks5() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://127.0.0.1:9050".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9050,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +568,21 @@ async fn fetch_idle(
|
||||
};
|
||||
|
||||
if folder_config == Config::ConfiguredInboxFolder {
|
||||
let mvbox;
|
||||
let syncbox = match ctx.should_move_sync_msgs().await? {
|
||||
false => &watch_folder,
|
||||
true => {
|
||||
mvbox = ctx.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
mvbox.as_deref().unwrap_or(&watch_folder)
|
||||
}
|
||||
};
|
||||
session
|
||||
.send_sync_msgs(ctx, syncbox)
|
||||
.await
|
||||
.context("fetch_idle: send_sync_msgs")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
|
||||
session
|
||||
.store_seen_flags_on_imap(ctx)
|
||||
.await
|
||||
|
||||
@@ -109,7 +109,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context
|
||||
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
|
||||
.await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
@@ -124,7 +124,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
// parameters used: a=n=i=s=
|
||||
if sync_token {
|
||||
context.sync_qr_code_tokens(None).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
|
||||
@@ -142,7 +142,7 @@ impl Smtp {
|
||||
{
|
||||
Ok(transport) => transport,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP failed to connect: {err:#}.");
|
||||
warn!(context, "SMTP failed to connect and authenticate: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -485,7 +485,6 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
let ratelimited = if context.ratelimit.read().await.can_send() {
|
||||
// add status updates and sync messages to end of sending queue
|
||||
context.flush_status_updates().await?;
|
||||
context.send_sync_msg().await?;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_smtp::{SmtpClient, SmtpTransport};
|
||||
use tokio::io::BufStream;
|
||||
use tokio::io::{AsyncBufRead, AsyncWrite, BufStream};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
@@ -28,6 +28,23 @@ fn alpn(port: u16) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
// Constructs a new SMTP transport
|
||||
// over a stream with already skipped SMTP greeting.
|
||||
async fn new_smtp_transport<S: AsyncBufRead + AsyncWrite + Unpin>(
|
||||
stream: S,
|
||||
) -> Result<SmtpTransport<S>> {
|
||||
// We always read the greeting manually to unify
|
||||
// the cases of STARTTLS where the greeting is
|
||||
// sent outside the encrypted channel and implicit TLS
|
||||
// where the greeting is sent after establishing TLS channel.
|
||||
let client = SmtpClient::new().smtp_utf8(true).without_greeting();
|
||||
|
||||
let transport = SmtpTransport::new(client, stream)
|
||||
.await
|
||||
.context("Failed to send EHLO command")?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn connect_and_auth(
|
||||
context: &Context,
|
||||
@@ -39,17 +56,17 @@ pub(crate) async fn connect_and_auth(
|
||||
user: &str,
|
||||
password: &str,
|
||||
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
|
||||
let session_stream =
|
||||
connect_stream(context, proxy_config.clone(), strict_tls, candidate).await?;
|
||||
let client = async_smtp::SmtpClient::new()
|
||||
.smtp_utf8(true)
|
||||
.without_greeting();
|
||||
let mut transport = SmtpTransport::new(client, session_stream).await?;
|
||||
let session_stream = connect_stream(context, proxy_config.clone(), strict_tls, candidate)
|
||||
.await
|
||||
.context("SMTP failed to connect")?;
|
||||
let mut transport = new_smtp_transport(session_stream).await?;
|
||||
|
||||
// Authenticate.
|
||||
let (creds, mechanism) = if oauth2 {
|
||||
// oauth2
|
||||
let access_token = get_oauth2_access_token(context, addr, password, false).await?;
|
||||
let access_token = get_oauth2_access_token(context, addr, password, false)
|
||||
.await
|
||||
.context("SMTP failed to get OAUTH2 access token")?;
|
||||
if access_token.is_none() {
|
||||
bail!("SMTP OAuth 2 error {}", addr);
|
||||
}
|
||||
@@ -70,7 +87,10 @@ pub(crate) async fn connect_and_auth(
|
||||
],
|
||||
)
|
||||
};
|
||||
transport.try_login(&creds, &mechanism).await?;
|
||||
transport
|
||||
.try_login(&creds, &mechanism)
|
||||
.await
|
||||
.context("SMTP failed to login")?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
@@ -177,16 +197,19 @@ async fn skip_smtp_greeting<R: tokio::io::AsyncBufReadExt + Unpin>(stream: &mut
|
||||
let mut line = String::with_capacity(512);
|
||||
loop {
|
||||
line.clear();
|
||||
let read = stream.read_line(&mut line).await?;
|
||||
let read = stream
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.context("Failed to read from stream while waiting for SMTP greeting")?;
|
||||
if read == 0 {
|
||||
bail!("Unexpected EOF while reading SMTP greeting.");
|
||||
bail!("Unexpected EOF while reading SMTP greeting");
|
||||
}
|
||||
if line.starts_with("220-") {
|
||||
continue;
|
||||
} else if line.starts_with("220 ") {
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("Unexpected greeting: {line:?}.");
|
||||
bail!("Unexpected greeting: {line:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,8 +243,9 @@ async fn connect_starttls_proxy(
|
||||
.await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let client = SmtpClient::new().smtp_utf8(true);
|
||||
let transport = SmtpTransport::new(client, BufStream::new(proxy_stream)).await?;
|
||||
let mut buffered_stream = BufStream::new(proxy_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let transport = new_smtp_transport(buffered_stream).await?;
|
||||
let tcp_stream = transport.starttls().await?.into_inner();
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
|
||||
.await
|
||||
@@ -264,8 +288,9 @@ async fn connect_starttls(
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let client = async_smtp::SmtpClient::new().smtp_utf8(true);
|
||||
let transport = async_smtp::SmtpTransport::new(client, BufStream::new(tcp_stream)).await?;
|
||||
let mut buffered_stream = BufStream::new(tcp_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let transport = new_smtp_transport(buffered_stream).await?;
|
||||
let tcp_stream = transport.starttls().await?.into_inner();
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
|
||||
@@ -991,6 +991,20 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 119)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"CREATE TABLE imap_send (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mime TEXT NOT NULL, -- Message content
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
|
||||
attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
)",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
28
src/sync.rs
28
src/sync.rs
@@ -120,7 +120,7 @@ impl Context {
|
||||
/// Adds most recent qr-code tokens for the given group or self-contact to the list of items to
|
||||
/// be synced. If device synchronization is disabled,
|
||||
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||
/// The caller should perform `SchedulerState::interrupt_smtp()` on its own to trigger sending.
|
||||
/// The caller should call `SchedulerState::interrupt_inbox()` on its own to trigger sending.
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, grpid: Option<&str>) -> Result<()> {
|
||||
if !self.should_send_sync_msgs().await? {
|
||||
return Ok(());
|
||||
@@ -153,7 +153,7 @@ impl Context {
|
||||
grpid: None,
|
||||
}))
|
||||
.await?;
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -233,17 +233,6 @@ impl Context {
|
||||
.body(json)
|
||||
}
|
||||
|
||||
/// Deletes IDs as returned by `build_sync_json()`.
|
||||
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
|
||||
self.sql
|
||||
.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({ids});"),
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes a JSON string created by `build_sync_json()`
|
||||
/// and construct `SyncItems` from it.
|
||||
pub(crate) fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
|
||||
@@ -384,7 +373,12 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(t.build_sync_json().await?.is_some());
|
||||
t.delete_sync_ids(ids).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({ids})"),
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized)?;
|
||||
@@ -565,7 +559,7 @@ mod tests {
|
||||
|
||||
// let alice's other device receive and execute the sync message,
|
||||
// also here, self-talk should stay hidden
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let sent_msg = alice.pop_sent_sync_msg().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.recv_msg_trash(&sent_msg).await;
|
||||
@@ -593,7 +587,7 @@ mod tests {
|
||||
.set_config(Config::Displayname, Some("Alice Human"))
|
||||
.await?;
|
||||
alice.send_sync_msg().await?;
|
||||
alice.pop_sent_msg().await;
|
||||
alice.pop_sent_sync_msg().await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.text, "hi");
|
||||
|
||||
@@ -628,7 +622,7 @@ mod tests {
|
||||
// group is promoted for compatibility (because the group could be created by older Core).
|
||||
// TODO: assert!(msg_id.is_none());
|
||||
assert!(msg_id.is_some());
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let sent = alice.pop_sent_sync_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
let mut sync_items = msg.sync_items.unwrap().items;
|
||||
assert_eq!(sync_items.len(), 1);
|
||||
|
||||
@@ -438,7 +438,7 @@ impl TestContext {
|
||||
/// Retrieves a sent message from the jobs table.
|
||||
///
|
||||
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
||||
/// table. Messages are returned in the order they have been sent.
|
||||
/// table. Messages are returned in the reverse order of sending.
|
||||
///
|
||||
/// Panics if there is no message or on any error.
|
||||
pub async fn pop_sent_msg(&self) -> SentMessage<'_> {
|
||||
@@ -532,6 +532,46 @@ impl TestContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves a sent sync message from the db.
|
||||
///
|
||||
/// This retrieves and removes a sync message which has been scheduled to send from the jobs
|
||||
/// table. Messages are returned in the order they have been sent.
|
||||
///
|
||||
/// Panics if there is no message or on any error.
|
||||
pub async fn pop_sent_sync_msg(&self) -> SentMessage<'_> {
|
||||
let (id, msg_id, payload) = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT id, msg_id, mime \
|
||||
FROM imap_send \
|
||||
ORDER BY id",
|
||||
(),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
Ok((rowid, msg_id, mime))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("query_row failed");
|
||||
self.ctx
|
||||
.sql
|
||||
.execute("DELETE FROM imap_send WHERE id=?", (id,))
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
|
||||
.await
|
||||
.expect("failed to update message state");
|
||||
SentMessage {
|
||||
payload,
|
||||
sender_msg_id: msg_id,
|
||||
sender_context: &self.ctx,
|
||||
recipients: self.get_primary_self_addr().await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a message.
|
||||
///
|
||||
/// Parsing a message does not run the entire receive pipeline, but is not without
|
||||
@@ -988,30 +1028,39 @@ impl SentMessage<'_> {
|
||||
///
|
||||
/// The keypair was created using the crate::key::tests::gen_key test.
|
||||
pub fn alice_keypair() -> KeyPair {
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
KeyPair::new(secret).unwrap()
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for bob@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn bob_keypair() -> KeyPair {
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/bob-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
KeyPair::new(secret).unwrap()
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for fiona@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn fiona_keypair() -> KeyPair {
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
KeyPair::new(secret).unwrap()
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
|
||||
/// Utility to help wait for and retrieve events.
|
||||
@@ -1132,7 +1181,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
|
||||
/// alice0's side that implies sending a sync message.
|
||||
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
|
||||
alice0.send_sync_msg().await.unwrap();
|
||||
let sync_msg = alice0.pop_sent_msg().await;
|
||||
let sync_msg = alice0.pop_sent_sync_msg().await;
|
||||
let no_msg = alice1.recv_msg_opt(&sync_msg).await;
|
||||
assert!(no_msg.is_none());
|
||||
}
|
||||
|
||||
34
src/tools.rs
34
src/tools.rs
@@ -272,30 +272,26 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
|
||||
|
||||
/* Message-ID tools */
|
||||
|
||||
/// Generate an ID. The generated ID should be as short and as unique as possible:
|
||||
/// - short, because it may also used as part of Message-ID headers or in QR codes
|
||||
/// - unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
|
||||
/// Generate an unique ID.
|
||||
///
|
||||
/// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
|
||||
/// The generated ID should be short but unique:
|
||||
/// - short, because it used in Message-ID and Chat-Group-ID headers and in QR codes
|
||||
/// - unique as two IDs generated on two devices should not be the same
|
||||
///
|
||||
/// Additional information when used as a message-id or group-id:
|
||||
/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as `Gr.<grpid>.<random>@<random>`
|
||||
/// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
|
||||
/// - the group-id should be a string with the characters [a-zA-Z0-9\-_]
|
||||
/// IDs generated by this function have 144 bits of entropy
|
||||
/// and are returned as 24 Base64 characters, each containing 6 bits of entropy.
|
||||
/// 144 is chosen because it is sufficiently secure
|
||||
/// (larger than AES-128 keys used for message encryption)
|
||||
/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character).
|
||||
pub(crate) fn create_id() -> String {
|
||||
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Generate 72 random bits.
|
||||
let mut arr = [0u8; 9];
|
||||
// Generate 144 random bits.
|
||||
let mut arr = [0u8; 18];
|
||||
rng.fill(&mut arr[..]);
|
||||
|
||||
// Take 11 base64 characters containing 66 random bits.
|
||||
base64::engine::general_purpose::URL_SAFE
|
||||
.encode(arr)
|
||||
.chars()
|
||||
.take(11)
|
||||
.collect()
|
||||
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
||||
}
|
||||
|
||||
/// Returns true if given string is a valid ID.
|
||||
@@ -311,7 +307,7 @@ pub(crate) fn validate_id(s: &str) -> bool {
|
||||
/// - the message ID should be globally unique
|
||||
/// - do not add a counter or any private data as this leaks information unnecessarily
|
||||
pub(crate) fn create_outgoing_rfc724_mid() -> String {
|
||||
format!("Mr.{}.{}@localhost", create_id(), create_id())
|
||||
format!("{}@localhost", create_id())
|
||||
}
|
||||
|
||||
// the returned suffix is lower-case
|
||||
@@ -925,7 +921,7 @@ DKIM Results: Passed=true";
|
||||
#[test]
|
||||
fn test_create_id() {
|
||||
let buf = create_id();
|
||||
assert_eq!(buf.len(), 11);
|
||||
assert_eq!(buf.len(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -961,7 +957,7 @@ DKIM Results: Passed=true";
|
||||
#[test]
|
||||
fn test_create_outgoing_rfc724_mid() {
|
||||
let mid = create_outgoing_rfc724_mid();
|
||||
assert!(mid.starts_with("Mr."));
|
||||
assert_eq!(mid.len(), 34);
|
||||
assert!(mid.ends_with("@localhost"));
|
||||
}
|
||||
|
||||
|
||||
13
test-data/key/alice-public.asc
Normal file
13
test-data/key/alice-public.asc
Normal file
@@ -0,0 +1,13 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcB
|
||||
KAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossj
|
||||
tTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
|
||||
AAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pM
|
||||
APkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQB
|
||||
l1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQY
|
||||
FggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRD
|
||||
Lo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyM
|
||||
h2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
=t/Qq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
30
test-data/key/bob-public.asc
Normal file
30
test-data/key/bob-public.asc
Normal file
@@ -0,0 +1,30 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA
|
||||
5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOe
|
||||
Jw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeT
|
||||
xc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+
|
||||
x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJ
|
||||
R2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5u
|
||||
ZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn2
|
||||
4RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEF
|
||||
LFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2c
|
||||
nZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbj
|
||||
mGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZn
|
||||
DztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C
|
||||
/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3O
|
||||
wE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhN
|
||||
JE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2Z
|
||||
YK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3d
|
||||
MA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp
|
||||
1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6Unx
|
||||
XGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIb
|
||||
DBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXR
|
||||
AJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPB
|
||||
lqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6
|
||||
nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+q
|
||||
vKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAK
|
||||
soeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGl
|
||||
h7oJqgCjZuk=
|
||||
=14Cq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
30
test-data/key/fiona-public.asc
Normal file
30
test-data/key/fiona-public.asc
Normal file
@@ -0,0 +1,30 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2G
|
||||
JzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9
|
||||
431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/
|
||||
0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosia
|
||||
Xlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2F
|
||||
p8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxl
|
||||
Lm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQ
|
||||
v0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3D
|
||||
vMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPI
|
||||
c/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl
|
||||
3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZ
|
||||
TbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaW
|
||||
TgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/H
|
||||
zM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWN
|
||||
Joq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhD
|
||||
H4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQy
|
||||
ttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpf
|
||||
fUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMn
|
||||
pf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98
|
||||
AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCd
|
||||
s3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC
|
||||
/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo
|
||||
6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3Bd
|
||||
VCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOo
|
||||
mIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4
|
||||
sjYdVzODl8T0zQ==
|
||||
=2jSq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
Reference in New Issue
Block a user