Compare commits

..

11 Commits

Author SHA1 Message Date
link2xt
486ea3a358 chore(release): prepare for 1.144.0 2024-09-21 18:53:02 +00:00
link2xt
624ae86913 api!: make QR code type for proxy not specific to SOCKS5 (#5980) 2024-09-21 18:26:07 +00:00
link2xt
b47b96d5d6 chore(cargo): update iroh to 0.25
According to
<https://www.iroh.computer/blog/iroh-0-25-0-custom-protocols-for-all>
gossip now handles updating direct addresses automatically.
2024-09-20 22:56:24 +00:00
link2xt
f6b5c5d150 feat: generate 144-bit group IDs
Instead of generating 72 random bits
and reducing them to 66 bits of Base64 characters,
generate 144 bits (18 bytes)
which is exactly 24 Base64 characters.

This should still be accepted by existing
Delta Chat clients which expect group ID
to be between 11 and 32 characters.

Message-ID creation is also simplified
to not have `Mr.` prefix
and dot in between two IDs.
Now it is a single ID followed by `@localhost`.

Some outdated documentation comments
are removed, e.g. group messages
don't start with `Gr.` already.
2024-09-20 22:38:28 +00:00
link2xt
9cc65c615c feat(smtp): more verbose SMTP connection establishment errors
The greeting is now always read manually,
even for STARTTLS connections,
so the errors returned on failure to read form the stream
are the same regardless of the connection type.
2024-09-20 20:37:47 +00:00
iequidoo
d6845bd5e9 feat: Use IMAP APPEND command to upload sync messages (#5845)
Why:
- With IMAP APPEND we can upload messages directly to the DeltaChat folder (for non-chatmail
  accounts).
- We can set the `\Seen` flag immediately so that if the user has other MUA, it doesn't alert about
  a new message if it's just a sync message (there were several such reports on the support
  forum). Though this also isn't useful for chatmail.
- We don't need SMTP envelope and overall remove some overhead on processing sync messages.
2024-09-20 17:07:45 -03:00
iequidoo
0b908db272 chore(deps): bump async-imap from 0.10.0 to 0.10.1 2024-09-20 17:07:45 -03:00
iequidoo
841ed43f11 feat: Don't put displayname into From/To/Sender if it equals to address (#5983)
If a displayname equals to the address, adding it looks excessive.
Moreover, it's not useful for Delta Chat receiving the message because
`sanitize_name_and_addr()` removes such a displayname anyway. Also now
at least DC Android requires specifying profile name, so there should be
a fallback for users having meaningful addresses to keep the old
behaviour when Core generates `From` w/o the profile name, and this
question has already appeared on the forum.
2024-09-20 15:59:33 -03:00
link2xt
60cd6f56be chore(cargo): update lazy_static to 1.5.0
This removes duplicate `spin` dependency.
2024-09-18 15:31:13 +00:00
link2xt
060fd55249 feat: HTTP(S) tunneling
HTTP proxy is tested with deltachat-repl
against local Privoxy
using
```
> set proxy_url http://127.0.0.1:8118/
> setqr dcaccount:https://nine.testrun.org/new
> configure
> connect
```
2024-09-18 10:52:31 +00:00
link2xt
38c7f7300e Partially revert "test(test-data): remove public keys that can be derived from secret keys" (#5977)
This reverts commit 1caf672904.

Otherwise public key signature is regenerated each time the key is
loaded and test `key::tests::test_load_self_existing` which loads the
key twice fails when two loads happen on different seconds.

Closes #5976
2024-09-18 09:48:01 +00:00
41 changed files with 1112 additions and 476 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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 }

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.143.0"
version = "1.144.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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 }

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.143.0"
"version": "1.144.0"
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.143.0"
"version": "1.144.0"
}

View File

@@ -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" },
]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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):

View File

@@ -1 +1 @@
2024-09-12
2024-09-21

View File

@@ -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);

View File

@@ -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?;

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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();

View File

@@ -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]

View File

@@ -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();

View File

@@ -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
View File

@@ -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(())
}
}

View File

@@ -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

View File

@@ -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={}",

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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"));
}

View 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-----

View 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-----

View 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-----