mirror of
https://github.com/chatmail/core.git
synced 2026-10-28 15:58:22 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2e477235 | ||
|
|
9e346ac650 | ||
|
|
4b8c65e7b4 | ||
|
|
4171c6f74a | ||
|
|
5a894d3e37 | ||
|
|
23b4ca05c8 | ||
|
|
e11a55c29f | ||
|
|
5ffcf2a4a4 | ||
|
|
baec90b70d | ||
|
|
da3ee2e025 | ||
|
|
a4de550a75 | ||
|
|
f3bb558ac3 | ||
|
|
8789098155 | ||
|
|
4ad53321f8 | ||
|
|
f0be6c387e | ||
|
|
4ce056c85c | ||
|
|
df19d9d1df | ||
|
|
ca4820aec3 | ||
|
|
a4363aa308 | ||
|
|
6767500ea2 | ||
|
|
dedc160a2a | ||
|
|
7a1df15f92 | ||
|
|
1a632bdade | ||
|
|
98289e8dbb | ||
|
|
706ee7a48c | ||
|
|
65da01f863 | ||
|
|
7f3cafd792 | ||
|
|
01fcfc17be | ||
|
|
52de603753 | ||
|
|
4d4ec874d2 | ||
|
|
41eed949ac | ||
|
|
53fe350ebb | ||
|
|
6bf5c07794 | ||
|
|
b4cc114195 | ||
|
|
e16e452091 | ||
|
|
26ec01deaa | ||
|
|
8e9c16baca | ||
|
|
8d2919d23e | ||
|
|
dd4641071e | ||
|
|
7a129352c8 | ||
|
|
38749e0e9c | ||
|
|
c827cb17c8 | ||
|
|
94641df57b | ||
|
|
5de39ba6a4 | ||
|
|
286c21a1e5 | ||
|
|
6dde761ea5 | ||
|
|
9637ecff6b | ||
|
|
dce6438d95 | ||
|
|
151f12b724 | ||
|
|
d62438d03c | ||
|
|
60e1d59182 | ||
|
|
64e58755c0 | ||
|
|
d766641ce2 | ||
|
|
5510fb5a18 | ||
|
|
036b928a7f | ||
|
|
f3b043f111 | ||
|
|
694ef8ba76 |
@@ -25,6 +25,8 @@ env:
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
|
||||
PYTHON_VERSION: "3.15"
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
@@ -268,11 +270,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
# Currently used Python version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -303,7 +305,7 @@ jobs:
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
@@ -324,11 +326,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: macos-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
- os: windows-latest
|
||||
python: 3.14
|
||||
python: latest
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -351,7 +353,7 @@ jobs:
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
|
||||
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## [2.65.0] - 2026-10-22
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- show info message for unpinning ([#8852](https://github.com/chatmail/core/pull/8852)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- don't ignore `join_next()` errors in `background_fetch_any()`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- remove outdated sentence saying QR scan aborts started securejoin processes.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- update webpki-roots to 1.0.10.
|
||||
- update webpki-roots 0.26.8 to 0.26.11.
|
||||
- update rustls-webpki 0.103.15 to 0.103.16.
|
||||
- add deny.toml exception for RUSTSEC-2026-0382.
|
||||
- remove unused broadcast icon.
|
||||
|
||||
### Refactor
|
||||
|
||||
- make `message::insert_tombstone()` synchronous.
|
||||
- make `insert_into_smtp()` synchronous.
|
||||
- make `non_chat_headers()` synchronous.
|
||||
- create all keyupdates in a single transaction.
|
||||
|
||||
## [2.64.0] - 2026-10-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- add jsonrpc for `get_summary_text`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- take key flags and expiration into account when selecting the key.
|
||||
- switch to another SMTP transport when current one returns an error.
|
||||
- do not spam channel summaries with reactions ([#8829](https://github.com/chatmail/core/pull/8829)).
|
||||
- do not copy Chat-Version header into unprotected headers.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Encode '@' with '%40' again in QR codes ([#8825](https://github.com/chatmail/core/pull/8825)).
|
||||
- abort scheduler tasks after shutdown timeout ([#8832](https://github.com/chatmail/core/pull/8832)).
|
||||
- do not apply channel reaction summaries as reactions.
|
||||
- imap: do not busy loop if there are no connection candidates and interrupt channel is closed.
|
||||
- races in `get_summary_text_ext`, improve perf.
|
||||
- avoid integer overflow when multiplying received image width by height.
|
||||
|
||||
### CI
|
||||
|
||||
- test with Python 3.15.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- bump rustls from 0.23.45 to 0.23.46.
|
||||
|
||||
### Refactor
|
||||
|
||||
- remove Context argument from `secret_key_to_public_key()`.
|
||||
- smtp: introduce smtp::Connection structure.
|
||||
- move `call_state_ext` to `CallInfo`.
|
||||
- clean up remaining `clippy::arithmetic_side_effects` in `mimeparser`.
|
||||
|
||||
## [2.63.0] - 2026-10-09
|
||||
|
||||
### API-Changes
|
||||
@@ -8983,3 +9049,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
|
||||
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
|
||||
[2.63.0]: https://github.com/chatmail/core/compare/v2.62.0..v2.63.0
|
||||
[2.64.0]: https://github.com/chatmail/core/compare/v2.63.0..v2.64.0
|
||||
[2.65.0]: https://github.com/chatmail/core/compare/v2.64.0..v2.65.0
|
||||
|
||||
Generated
+38
-29
@@ -334,13 +334,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.86"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -920,9 +920,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.0"
|
||||
version = "5.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
|
||||
checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
@@ -1334,7 +1334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1415,7 +1415,7 @@ dependencies = [
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"webpki-roots",
|
||||
"webpki-roots 1.0.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1440,7 +1440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1461,14 +1461,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1484,7 +1484,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1514,7 +1514,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2695,7 +2695,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3060,7 +3060,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"wasm-bindgen-futures",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"x509-parser",
|
||||
"z32",
|
||||
]
|
||||
@@ -3246,7 +3246,7 @@ dependencies = [
|
||||
"tokio-websockets",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"ws_stream_wasm",
|
||||
"z32",
|
||||
]
|
||||
@@ -5132,7 +5132,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -5289,9 +5289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
version = "0.23.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"brotli-decompressor",
|
||||
@@ -5299,7 +5299,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.15",
|
||||
"rustls-webpki 0.103.16",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5336,9 +5336,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
version = "0.103.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6309,9 +6309,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
version = "1.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -6923,9 +6923,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.8"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -7521,9 +7530,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6924db1f1011f1d22566c45c7090aa66c3107c3c463ca32beaa8b2327127040a"
|
||||
checksum = "3d7436d4f59711e0a796697630a77c33981c4b5406bdd52e874a08b27ad938ee"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 1.9.0",
|
||||
@@ -7541,9 +7550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc_derive"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42c374248c189b15a7f0660db3984e9dcc790ec15f4b44e932e92be49de829f9"
|
||||
checksum = "feab3bc81a5743c3661099ef6727b182a27c32686b78831e143786b636b32fee"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"darling",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -107,7 +107,7 @@ toml = "0.9"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
walkdir = "2.5.0"
|
||||
webpki-roots = "0.26.8"
|
||||
webpki-roots = "1.0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,149 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 128 128"
|
||||
viewBox="0 0 60 60"
|
||||
version="1.1"
|
||||
id="svg878"
|
||||
sodipodi:docname="icon-broadcast.svg"
|
||||
width="60"
|
||||
height="60"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
|
||||
inkscape:export-xdpi="409.60001"
|
||||
inkscape:export-ydpi="409.60001">
|
||||
<metadata
|
||||
id="metadata884">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs882" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1329"
|
||||
inkscape:window-height="847"
|
||||
id="namedview880"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.21875"
|
||||
inkscape:cx="36.598802"
|
||||
inkscape:cy="32.191617"
|
||||
inkscape:window-x="111"
|
||||
inkscape:window-y="205"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg878"
|
||||
inkscape:document-rotation="0" />
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="65.25"
|
||||
cy="89"
|
||||
r="26.440001"
|
||||
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#FFC107"
|
||||
offset="0"
|
||||
id="stop833" />
|
||||
<stop
|
||||
stop-color="#FFBD06"
|
||||
offset=".3502"
|
||||
id="stop835" />
|
||||
<stop
|
||||
stop-color="#FFB104"
|
||||
offset=".6938"
|
||||
id="stop837" />
|
||||
<stop
|
||||
stop-color="#FFA000"
|
||||
offset="1"
|
||||
id="stop839" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="b"
|
||||
cx="52.5"
|
||||
cy="19.75"
|
||||
r="92.975998"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)">
|
||||
<stop
|
||||
stop-color="#EF5350"
|
||||
offset="0"
|
||||
id="stop848" />
|
||||
<stop
|
||||
stop-color="#EB4F4C"
|
||||
offset=".246"
|
||||
id="stop850" />
|
||||
<stop
|
||||
stop-color="#E04341"
|
||||
offset=".4878"
|
||||
id="stop852" />
|
||||
<stop
|
||||
stop-color="#CD302F"
|
||||
offset=".7272"
|
||||
id="stop854" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset=".8004"
|
||||
id="stop856" />
|
||||
<stop
|
||||
stop-color="#C62828"
|
||||
offset="1"
|
||||
id="stop858" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="a"
|
||||
cx="16.979"
|
||||
cy="92"
|
||||
r="24.165001"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45.323856,68.997115,75.979538)"
|
||||
xlink:href="#b">
|
||||
<stop
|
||||
stop-color="#E0E0E0"
|
||||
offset="0"
|
||||
id="stop863" />
|
||||
<stop
|
||||
stop-color="#CFCFCF"
|
||||
offset=".3112"
|
||||
id="stop865" />
|
||||
<stop
|
||||
stop-color="#A4A4A4"
|
||||
offset=".9228"
|
||||
id="stop867" />
|
||||
<stop
|
||||
stop-color="#9E9E9E"
|
||||
offset="1"
|
||||
id="stop869" />
|
||||
</radialGradient>
|
||||
<rect
|
||||
y="0"
|
||||
x="0"
|
||||
height="60"
|
||||
width="60"
|
||||
id="rect1420"
|
||||
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
|
||||
<path
|
||||
id="path872"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
|
||||
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -31,7 +31,7 @@ use deltachat::internals_for_benches::create_broadcast_secret;
|
||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
||||
use deltachat::securejoin::get_securejoin_qr;
|
||||
use deltachat::{
|
||||
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
|
||||
Events, chat::ChatId, context::Context, internals_for_benches::key_from_asc,
|
||||
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
|
||||
stock_str::StockStrings,
|
||||
};
|
||||
@@ -57,10 +57,6 @@ async fn create_context() -> Context {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
|
||||
.await
|
||||
.unwrap();
|
||||
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
store_self_keypair(&context, &secret)
|
||||
.await
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::path::PathBuf;
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::{
|
||||
Events,
|
||||
config::Config,
|
||||
context::Context,
|
||||
imex::{ImexMode, imex},
|
||||
receive_imf::receive_imf,
|
||||
@@ -109,16 +108,6 @@ async fn create_context() -> Context {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let addr = "alice@example.com";
|
||||
context.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
context
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -389,9 +389,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
/**
|
||||
* Configure the context. The configuration is handled by key=value pairs as:
|
||||
*
|
||||
* - `configured_addr` = Email address in use.
|
||||
* Unless for testing, do not set this value using dc_set_config().
|
||||
* Instead, set `addr` and call dc_configure().
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
@@ -468,9 +465,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
|
||||
* however, are not handled by the core otherwise.
|
||||
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop.
|
||||
* 1 = WebXDC realtime API is enabled (default).
|
||||
* - `who_can_call_me` = Who can cause call notifications.
|
||||
* 0 = Everybody (except explicitly blocked contacts),
|
||||
* 1 = Contacts (default, does not include contact requests),
|
||||
@@ -481,8 +475,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* if you want to use the deprecated dc_configure() API, such as:
|
||||
*
|
||||
* - `addr` = Email address to use for configuration.
|
||||
* If dc_configure() fails this is not the email address actually in use.
|
||||
* Use `configured_addr` to find out the email address actually in use.
|
||||
* - `mail_server` = IMAP-server, guessed if left out
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
@@ -2613,8 +2605,6 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
||||
* During the handshake, info messages are added to the chat,
|
||||
* showing progress, success or errors.
|
||||
*
|
||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://securejoin.delta.chat/ for details about both protocols.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -4478,6 +4468,7 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
* - DC_INFO_GROUP_DESCRIPTION_CHANGED (70) - Info-message "Description changed", UI should open the profile with the description
|
||||
* - DC_INFO_MESSAGE_PINNED (71) - Message pinned, UI should scroll to the pinned message returned by dc_msg_get_parent()
|
||||
* - DC_INFO_MESSAGE_UNPINNED (72) - Message unpinned, UI should scroll to the pinned message returned by dc_msg_get_parent()
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4538,6 +4529,7 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
|
||||
#define DC_INFO_MESSAGE_PINNED 71
|
||||
#define DC_INFO_MESSAGE_UNPINNED 72
|
||||
|
||||
|
||||
/**
|
||||
@@ -6474,7 +6466,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* The event is emitted on the device modifying the transports
|
||||
* as well as on other devices applying the synced change,
|
||||
* for the JSON-RPC calls `add_or_update_transport`, `add_transport_from_qr`,
|
||||
* `delete_transport` or `set_config(configured_addr)`.
|
||||
* `delete_transport`.
|
||||
*/
|
||||
#define DC_EVENT_TRANSPORTS_MODIFIED 2600
|
||||
|
||||
@@ -7275,6 +7267,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated 2026-08-31
|
||||
#define DC_STR_PHASING_OUT 245
|
||||
|
||||
/// "You unpinned a message."
|
||||
#define DC_STR_MESSAGE_UNPINNED_BY_YOU 246
|
||||
|
||||
/// "Message unpinned by %1$s."
|
||||
#define DC_STR_MESSAGE_UNPINNED_BY_OTHER 247
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.63.0"
|
||||
"version": "2.66.0-dev"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -495,6 +495,15 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds fake transport that cannot be used to connect.
|
||||
///
|
||||
/// Used for offline tests only.
|
||||
async fn add_pseudo_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let context = self.get_context(account_id).await?;
|
||||
deltachat::transport::add_pseudo_transport(&context, &addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
/// and adds it as a transport.
|
||||
///
|
||||
@@ -912,8 +921,6 @@ impl CommandApi {
|
||||
/// During the handshake, info messages are added to the chat,
|
||||
/// showing progress, success or errors.
|
||||
///
|
||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
///
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
///
|
||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
@@ -1451,6 +1458,36 @@ impl CommandApi {
|
||||
MsgId::new(message_id).get_html(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns a message summary.
|
||||
/// Used for notifications, pinned messages and for copying texts to clipboard.
|
||||
///
|
||||
/// - `add_forwarded`: `true`=add a hint if a message was forwarded.
|
||||
/// `false`=do not add the hint;
|
||||
/// this is allowed only if the message is shown without a username, e.g. for pinned messages.
|
||||
/// - `add_type_emoji`: `true`=add the type of the message as an emoji prefix.
|
||||
/// `false`=do not add type emoji;
|
||||
/// this is allowed only if the message type is visible otherwise,
|
||||
/// close to where the emoji would be displayed otherwise
|
||||
/// (eg. images in pinned messages preceding summary)
|
||||
/// - `approx_chars`: `>0`=convert linebreaks to spaces and truncate the line at about the given number of characters;
|
||||
/// for single-line UI elements such as pinned messages or notifications;
|
||||
/// additionally, UIs will most times clip the text to the available space.
|
||||
/// `0`=leave linebreaks as is and return the full text, useful e.g. for copying to clipboard.
|
||||
async fn get_message_summary_text(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
add_forwarded: bool,
|
||||
add_type_emoji: bool,
|
||||
approx_chars: u32,
|
||||
) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = Message::load_from_db(&ctx, MsgId::new(message_id)).await?;
|
||||
Ok(msg
|
||||
.get_summary_text_ext(&ctx, add_forwarded, add_type_emoji, approx_chars as usize)
|
||||
.await)
|
||||
}
|
||||
|
||||
/// get multiple messages in one call,
|
||||
/// if loading one message fails the error is stored in the result object in it's place.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
@@ -18,6 +18,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python :: 3.15",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -139,6 +139,11 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def add_pseudo_transport(self, addr: str):
|
||||
"""Add transport for offline tests without connection candidates."""
|
||||
yield self._rpc.add_pseudo_transport.future(self.id, addr)
|
||||
|
||||
@futuremethod
|
||||
def init_transports(self, qr: Optional[str] = None):
|
||||
"""Add an initial transport on the chatmail relay that answers fastest.
|
||||
@@ -376,7 +381,6 @@ class Account:
|
||||
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
See https://securejoin.delta.chat/ for protocol details.
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
|
||||
@@ -87,6 +87,31 @@ def test_second_device(acf, alice_and_remote_bob) -> None:
|
||||
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||
|
||||
|
||||
def test_old_device_delete_transport(acf, alice_and_remote_bob) -> None:
|
||||
"""Test setting up old core as the second device for the new core and deleting a transport.
|
||||
|
||||
Old core unfortunately expects that deprecated configured_addr config value is set
|
||||
and fails to delete a transport otherwise. The test makes sure new core sets some configured_addr
|
||||
for backwards compatibility."""
|
||||
alice, _alice_contact_bob, remote_eval = alice_and_remote_bob("2.62.0")
|
||||
|
||||
alice.set_config("displayname", "Alice123")
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
alice_provide_backup_future = alice._rpc.provide_backup.future(alice.id)
|
||||
qr = alice._rpc.get_backup_qr(alice.id)
|
||||
|
||||
remote_eval("locals().setdefault('new_account', bob.manager.add_account())")
|
||||
remote_eval(f"new_account._rpc.get_backup(new_account.id, {qr!r})")
|
||||
alice_provide_backup_future()
|
||||
|
||||
assert alice.get_config("displayname") == remote_eval("new_account.get_config('displayname')")
|
||||
|
||||
first_transport_addr = alice.list_transports()[0]["addr"]
|
||||
remote_eval(f"new_account.delete_transport({first_transport_addr!r})")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("replace_relay", [False, True], ids=["add", "replace"])
|
||||
def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, replace_relay):
|
||||
"""Test 2.48 Bob learns a relay change of Alice from a keyupdate, and is shown nothing."""
|
||||
@@ -104,7 +129,6 @@ def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, repla
|
||||
# an accepted chat alone is not enough, a message must have flowed.
|
||||
alice_chat = alice_contact_bob.create_chat()
|
||||
alice.set_config("keyupdate_debounce", "1")
|
||||
old_addr = alice.get_config("configured_addr")
|
||||
alice_chat.send_text("hi")
|
||||
assert remote_eval("bob.wait_for_incoming_msg().get_snapshot().text") == "hi"
|
||||
before = bob_sees()
|
||||
@@ -113,8 +137,9 @@ def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, repla
|
||||
# and signature timestamps have one-second resolution:
|
||||
# without waiting, the re-signed key can tie with the copy Bob holds, keeping his.
|
||||
time.sleep(2)
|
||||
old_addr = alice.list_transports()[0]["addr"]
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
(new_addr,) = [t["addr"] for t in alice.list_transports() if t["addr"] != old_addr]
|
||||
new_addr = alice.list_transports()[1]["addr"]
|
||||
if replace_relay:
|
||||
alice.delete_transport(old_addr)
|
||||
alice.bring_online()
|
||||
|
||||
@@ -50,8 +50,6 @@ SETUP_DATA = b"realtime-setup"
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc, wait=True):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
|
||||
@@ -45,10 +45,8 @@ def test_add_second_address(acf) -> None:
|
||||
second_addr = account.list_transports()[1]["addr"]
|
||||
third_addr = account.list_transports()[2]["addr"]
|
||||
|
||||
assert account.get_config("configured_addr") == first_addr
|
||||
account.delete_transport(first_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
assert account.get_config("configured_addr") != first_addr
|
||||
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 1
|
||||
@@ -187,9 +185,6 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
wait_for_io_started(ac1)
|
||||
[transport1, transport3] = ac1.list_transports()
|
||||
|
||||
log.section("ac1 changes the sending transport")
|
||||
ac1.set_config("configured_addr", transport3["addr"])
|
||||
|
||||
log.section("ac1 removes the first transport")
|
||||
ac1.delete_transport(transport1["addr"])
|
||||
|
||||
@@ -197,7 +192,6 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
wait_for_io_started(ac1_clone)
|
||||
[transport3] = ac1_clone.list_transports()
|
||||
assert transport3["addr"] == addr3
|
||||
assert ac1_clone.get_config("configured_addr") == addr3
|
||||
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
ac2_chat.send_text("Hello!")
|
||||
@@ -206,8 +200,8 @@ def test_transport_synchronization(acf, log) -> None:
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||
|
||||
|
||||
def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
"""Test that a transport promoted on one device is usable on other devices."""
|
||||
def test_receive_message_after_transport_sync(acf) -> None:
|
||||
"""Test that second device can receive messages after transport synchronization."""
|
||||
ac1, bob = acf.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
@@ -221,12 +215,7 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
[transport1, transport2] = ac1_transports
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert len(ac1_clone.list_transports()) == 2
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
ac1_chat.send_text("Hello!")
|
||||
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||
@@ -236,22 +225,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||
|
||||
|
||||
def test_recognize_self_address(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.chat == alice.create_chat(bob)
|
||||
|
||||
|
||||
def test_transport_limit(acf) -> None:
|
||||
"""Test transports limit."""
|
||||
account = acf.get_online_account()
|
||||
@@ -294,7 +267,6 @@ def test_message_info_imap_urls(acf) -> None:
|
||||
|
||||
# Alice switches to another transport and removes the rest of the transports.
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
removed_addrs = []
|
||||
for transport in alice.list_transports():
|
||||
if transport["addr"] != new_alice_addr:
|
||||
@@ -326,7 +298,6 @@ def test_remove_primary_transport(acf, log) -> None:
|
||||
|
||||
log.section("Alice removes the primary relay")
|
||||
alice.delete_transport(transport1["addr"])
|
||||
assert alice.get_config("configured_addr") == transport2["addr"]
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
@@ -369,7 +340,6 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
|
||||
def test_background_fetch_from_second_transport(acf, direct_imap, dc):
|
||||
alice, alice_chat, bob_chat = alice_with_two_transports_and_bob(acf)
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
assert alice.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
alice.stop_io()
|
||||
bob_chat.send_text("hello")
|
||||
|
||||
@@ -647,9 +647,8 @@ def test_qr_scan_updates_new_relay_address(acf):
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
for ac in [alice, bob]:
|
||||
old_addr = ac.get_config("configured_addr")
|
||||
old_addr = ac.list_transports()[0]["addr"]
|
||||
ac.add_transport_from_qr(acf.get_account_qr())
|
||||
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
|
||||
ac.delete_transport(old_addr)
|
||||
|
||||
bob.secure_join(alice.get_qr_code())
|
||||
|
||||
@@ -100,7 +100,6 @@ def test_lowercase_address(acf) -> None:
|
||||
)
|
||||
assert account.is_configured()
|
||||
assert addr_upper != addr
|
||||
assert account.get_config("configured_addr") == addr
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
|
||||
param = account.get_info()["used_transport_settings"]
|
||||
@@ -168,7 +167,7 @@ def test_list_transports(acf) -> None:
|
||||
def test_account(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob_addr = bob.list_transports()[0]["addr"]
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
@@ -331,7 +330,7 @@ def test_chat(acf) -> None:
|
||||
def test_contact(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob_addr = bob.list_transports()[0]["addr"]
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
@@ -1489,3 +1488,19 @@ def test_is_sending_finished(dc, acf) -> None:
|
||||
bob.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
|
||||
assert dc.is_sending_finished()
|
||||
|
||||
|
||||
def test_pseudo_transport_start_stop_io(acf):
|
||||
"""
|
||||
Test that I/O can be started and stopped for pseudo transport.
|
||||
|
||||
Pseudo transport has no connection candidates,
|
||||
so in the past this resulted in IMAP busy looping
|
||||
and stop_io() failing to even cancel the loop sometimes.
|
||||
This is a regression test for this bug.
|
||||
"""
|
||||
for i in range(10):
|
||||
alice = acf.get_unconfigured_account()
|
||||
alice.add_pseudo_transport(f"alice{i}@example.net")
|
||||
alice.start_io()
|
||||
alice.stop_io()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.63.0"
|
||||
"version": "2.66.0-dev"
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0382>
|
||||
"RUSTSEC-2026-0049",
|
||||
"RUSTSEC-2026-0098",
|
||||
"RUSTSEC-2026-0099",
|
||||
"RUSTSEC-2026-0382",
|
||||
|
||||
# Panic in CRL signature checks.
|
||||
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||
@@ -54,6 +56,14 @@ ignore = [
|
||||
# We enable PQC for encryption rather than signatures.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
|
||||
"RUSTSEC-2025-0144",
|
||||
|
||||
# Timing side channels in crypto-bigint 0.5.5, fixed only in 0.7.6.
|
||||
# rPGP 0.21 depends on elliptic-curve 0.13 and cx448 0.1,
|
||||
# which require crypto-bigint^0.5.
|
||||
# Delta by default uses only Ed25519 and X25519 (and RSA keys from old profiles)
|
||||
# that do not use crypto-bigint.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0387>
|
||||
"RUSTSEC-2026-0387",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -98,6 +108,7 @@ skip = [
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "webpki-roots", version = "0.26.8" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
@@ -127,6 +138,7 @@ allow = [
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0", # Boost Software License 1.0
|
||||
"CC0-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
|
||||
@@ -471,6 +471,19 @@ CREATE TABLE smtp_success (
|
||||
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
-- Similar to smtp_success, but records failures.
|
||||
CREATE TABLE smtp_failure (
|
||||
-- Sequentially increasing ID of the failure
|
||||
-- Transport with the highest ID is to be used last.
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
|
||||
-- ID of the transport that was used to attempt sending a message.
|
||||
transport_id INTEGER UNIQUE NOT NULL,
|
||||
|
||||
-- Delete `smtp_failure` rows when the transport is deleted.
|
||||
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
-- Table of "sync items" to be grouped into sync messages
|
||||
-- and sent to own devices.
|
||||
CREATE TABLE multi_device_sync (
|
||||
@@ -710,6 +723,7 @@ CREATE TABLE imap (
|
||||
CREATE INDEX imap_folder ON imap(transport_id, folder);
|
||||
CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
|
||||
CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid);
|
||||
CREATE INDEX imap_pending_deletions ON imap(transport_id, folder) WHERE target != folder;
|
||||
|
||||
CREATE TABLE imap_markseen (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.63.0"
|
||||
version = "2.66.0-dev"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -370,12 +370,7 @@ class ACFactory:
|
||||
acname = ac._logid
|
||||
addr = f"{acname}@offline.org"
|
||||
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
|
||||
ac.update_config(
|
||||
{
|
||||
"configured_addr": addr,
|
||||
"displayname": acname,
|
||||
},
|
||||
)
|
||||
ac.set_config("displayname", acname)
|
||||
self._preconfigure_key(ac)
|
||||
self._acsetup.init_logging(ac)
|
||||
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
|
||||
|
||||
@@ -51,7 +51,6 @@ class TestGroupStressTests:
|
||||
|
||||
lp.sec("ac2: receiving system message about contact addition")
|
||||
sysmsg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac5.get_config("configured_addr") in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 4
|
||||
|
||||
lp.sec("ac5: waiting for message about addition to the chat")
|
||||
|
||||
@@ -132,7 +132,7 @@ class TestOfflineContact:
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("configured_addr"))
|
||||
contact1 = ac1.create_contact(ac1.get_self_contact().addr)
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
@@ -223,7 +223,7 @@ class TestOfflineChat:
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("configured_addr")
|
||||
assert contact.addr == ac2.get_self_contact().addr
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
@@ -456,7 +456,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_self_contact().addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -552,7 +552,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_self_contact().addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -604,7 +604,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_self_contact().addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -621,7 +621,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
assert contact2.addr == ac_contact.get_self_contact().addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-10-09
|
||||
2026-10-22
|
||||
+44
-40
@@ -123,6 +123,45 @@ impl CallInfo {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns call state.
|
||||
pub fn call_state(&self) -> CallState {
|
||||
if self.is_incoming() {
|
||||
if self.is_accepted() {
|
||||
if self.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: self.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if self.is_canceled() {
|
||||
// Call was explicitly canceled
|
||||
// by the caller before we picked it up.
|
||||
CallState::Missed
|
||||
} else if self.is_ended() {
|
||||
CallState::Declined
|
||||
} else if self.is_stale() {
|
||||
CallState::Missed
|
||||
} else {
|
||||
CallState::Alerting
|
||||
}
|
||||
} else if self.is_accepted() {
|
||||
if self.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: self.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if self.is_canceled() {
|
||||
CallState::Canceled
|
||||
} else if self.is_ended() || self.is_stale() {
|
||||
CallState::Declined
|
||||
} else {
|
||||
CallState::Alerting
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the call is accepted.
|
||||
pub fn is_accepted(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
@@ -508,10 +547,10 @@ impl Context {
|
||||
Ok(self.load_call_by_message(call))
|
||||
}
|
||||
|
||||
// Loads information about the call given the `Message`.
|
||||
//
|
||||
// If the `Message` is not a call message, returns `None`
|
||||
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
|
||||
/// Loads information about the call given the `Message`.
|
||||
///
|
||||
/// If the `Message` is not a call message, returns `None`
|
||||
pub fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
|
||||
if call.viewtype != Viewtype::Call {
|
||||
// This can happen e.g. if a "call accepted"
|
||||
// or "call ended" message is received
|
||||
@@ -581,42 +620,7 @@ pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
|
||||
.load_call_by_id(msg_id)
|
||||
.await?
|
||||
.with_context(|| format!("{msg_id} is not a call message"))?;
|
||||
let state = if call.is_incoming() {
|
||||
if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
// Call was explicitly canceled
|
||||
// by the caller before we picked it up.
|
||||
CallState::Missed
|
||||
} else if call.is_ended() {
|
||||
CallState::Declined
|
||||
} else if call.is_stale() {
|
||||
CallState::Missed
|
||||
} else {
|
||||
CallState::Alerting
|
||||
}
|
||||
} else if call.is_accepted() {
|
||||
if call.is_ended() {
|
||||
CallState::Completed {
|
||||
duration: call.duration_seconds(),
|
||||
}
|
||||
} else {
|
||||
CallState::Active
|
||||
}
|
||||
} else if call.is_canceled() {
|
||||
CallState::Canceled
|
||||
} else if call.is_ended() || call.is_stale() {
|
||||
CallState::Declined
|
||||
} else {
|
||||
CallState::Alerting
|
||||
};
|
||||
Ok(state)
|
||||
Ok(call.call_state())
|
||||
}
|
||||
|
||||
/// ICE server for JSON serialization.
|
||||
|
||||
+8
-10
@@ -2893,15 +2893,9 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
let (queued_msg, side_effects) = queued_msg_pair;
|
||||
|
||||
if needs_encryption && !is_encrypted {
|
||||
let addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let text = stock_str::unencrypted_email(
|
||||
context,
|
||||
addr.unwrap_or_default()
|
||||
.split('@')
|
||||
.nth(1)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let text =
|
||||
stock_str::unencrypted_email(context, addr.split('@').nth(1).unwrap_or_default()).await;
|
||||
message::set_msg_failed(context, msg, &text).await?;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
@@ -4692,7 +4686,11 @@ pub(crate) async fn resend_msgs_ext(
|
||||
// Broadcast owners shouldn't see spinners on messages being auto-re-sent to new
|
||||
// subscribers (otherwise big channel owners will see spinners most of the time).
|
||||
if to_fingerprint.is_none() {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
let sql = &context.sql;
|
||||
sql.transaction(|tx| {
|
||||
message::update_msg_state(tx, msg.id, MessageState::OutPending)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
msg_state => bail!("Unexpected message state {msg_state}"),
|
||||
|
||||
+4
-67
@@ -4,7 +4,7 @@ use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -197,11 +197,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// Deprecated(2026-09).
|
||||
///
|
||||
/// Use ConfiguredLoginParam and list_transports() instead.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
@@ -442,10 +437,6 @@ pub enum Config {
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
|
||||
/// Enable webxdc realtime features.
|
||||
#[strum(props(default = "1"))]
|
||||
WebxdcRealtimeEnabled,
|
||||
|
||||
/// Last device token stored on the chatmail server.
|
||||
///
|
||||
/// If it has not changed, we do not store
|
||||
@@ -752,34 +743,6 @@ impl Context {
|
||||
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
|
||||
.await?;
|
||||
}
|
||||
Config::ConfiguredAddr => {
|
||||
let Some(addr) = value else {
|
||||
bail!("Cannot unset configured_addr");
|
||||
};
|
||||
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('configured_addr', ?)",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
@@ -854,18 +817,6 @@ impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
/// Returns `false` if no addresses are configured.
|
||||
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
// Employ the config cache to optimize for `ConfiguredAddr` passed.
|
||||
if !addr.is_empty()
|
||||
&& addr_cmp(
|
||||
addr,
|
||||
&self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(self
|
||||
.get_self_addrs()
|
||||
.await?
|
||||
@@ -873,20 +824,6 @@ impl Context {
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the address used for sending.
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.clear();
|
||||
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||
.await?;
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all self addresses, newest first.
|
||||
pub(crate) async fn get_self_addrs(&self) -> Result<Vec<String>> {
|
||||
let query_only = true;
|
||||
@@ -895,10 +832,10 @@ impl Context {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the address of the transport used for sending.
|
||||
/// Returns an error if no self addr is configured.
|
||||
/// Returns address of the first transport.
|
||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
||||
self.get_config(Config::ConfiguredAddr)
|
||||
self.sql
|
||||
.query_get_value("SELECT addr FROM transports ORDER BY id LIMIT 1", ())
|
||||
.await?
|
||||
.context("No self addr configured")
|
||||
}
|
||||
|
||||
+8
-12
@@ -37,8 +37,8 @@ use crate::sync::Sync::Nosync;
|
||||
use crate::tools::time;
|
||||
use crate::transport::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
||||
purge_transport_caches, send_sync_transports, transport_addrs,
|
||||
ConnectionCandidate, delete_transport_row, purge_transport_caches, send_sync_transports,
|
||||
transport_addrs, update_configured_addr,
|
||||
};
|
||||
use crate::{EventType, autorelay, stock_str};
|
||||
|
||||
@@ -275,7 +275,7 @@ impl Context {
|
||||
/// another one is chosen automatically.
|
||||
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
|
||||
let now = time();
|
||||
let (removed_transport_id, reelected) = self
|
||||
let removed_transport_id = self
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
if transport_addrs(transaction)?.len() <= 1 {
|
||||
@@ -294,14 +294,10 @@ impl Context {
|
||||
let remove_timestamp = std::cmp::max(now, add_timestamp);
|
||||
let transport_id = delete_transport_row(transaction, addr, remove_timestamp)?
|
||||
.context("Transport disappeared")?;
|
||||
let reelected = maybe_update_sending_transport(transaction)?;
|
||||
Ok((transport_id, reelected))
|
||||
update_configured_addr(transaction)?;
|
||||
Ok(transport_id)
|
||||
})
|
||||
.await?;
|
||||
if let Some(new_addr) = reelected {
|
||||
info!(self, "Using transport {new_addr:?} for sending now.");
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
send_sync_transports(self).await?;
|
||||
purge_transport_caches(self, removed_transport_id).await;
|
||||
// Restarting all IO also stops the removed transport's IMAP loop.
|
||||
@@ -513,6 +509,8 @@ pub(crate) async fn configure(
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
|
||||
let transport_id = 0;
|
||||
|
||||
let proxy_config2 = proxy_config.clone();
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
@@ -522,6 +520,7 @@ pub(crate) async fn configure(
|
||||
&smtp_password,
|
||||
&proxy_config2,
|
||||
&smtp_addr,
|
||||
transport_id,
|
||||
strict_tls,
|
||||
)
|
||||
.await?;
|
||||
@@ -533,7 +532,6 @@ pub(crate) async fn configure(
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
let transport_id = 0;
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
|
||||
let imap_session = match imap.connect(ctx).await {
|
||||
@@ -799,8 +797,6 @@ mod tests {
|
||||
let t = &tcm.unconfigured().await;
|
||||
|
||||
add_pseudo_transport(t, "primary@example.org").await?;
|
||||
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
t.check_relay_limit().await?;
|
||||
|
||||
|
||||
+27
-38
@@ -372,7 +372,11 @@ pub(crate) async fn import_public_key(
|
||||
.await?
|
||||
{
|
||||
let old_public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
merged_public_key = merge_openpgp_certificates(public_key.clone(), old_public_key)
|
||||
|
||||
// Order of merge_openpgp_certificates() arguments is important.
|
||||
// Second argument is the new key, it is preferred in case of ties,
|
||||
// i.e. same signature timestamps.
|
||||
merged_public_key = merge_openpgp_certificates(old_public_key, public_key.clone())
|
||||
.context("Failed to merge public keys")?;
|
||||
&merged_public_key
|
||||
} else {
|
||||
@@ -718,9 +722,9 @@ impl Contact {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
.get_primary_self_addr()
|
||||
.await
|
||||
.context("Cannot get address for self-contact")?;
|
||||
if let Some(self_fp) = self_fingerprint_opt(context).await? {
|
||||
contact.fingerprint = Some(self_fp.to_string());
|
||||
}
|
||||
@@ -1421,10 +1425,6 @@ WHERE addr=?
|
||||
);
|
||||
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let Some(fingerprint_other) = contact.fingerprint() else {
|
||||
return Ok(stock_str::encr_none(context));
|
||||
@@ -1444,37 +1444,15 @@ WHERE addr=?
|
||||
.await?
|
||||
.dc_fingerprint()
|
||||
.human_readable();
|
||||
if addr < contact.addr {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context),
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
if fingerprint_self < fingerprint_other {
|
||||
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
|
||||
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context),
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
);
|
||||
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
|
||||
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
|
||||
}
|
||||
|
||||
if let Some(public_key) = contact.public_key(context).await?
|
||||
&& let Some(relay_addrs) = addresses_from_public_key(&public_key)
|
||||
{
|
||||
if let Some(relay_addrs) = contact.relay_addresses(context).await? {
|
||||
ret += "\n\nRelays:";
|
||||
for relay in &relay_addrs {
|
||||
ret += "\n";
|
||||
@@ -1604,6 +1582,17 @@ WHERE addr=?
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of relay addresses for the contact if it is distributed in the public key.
|
||||
pub(crate) async fn relay_addresses(&self, context: &Context) -> Result<Option<Vec<String>>> {
|
||||
if let Some(public_key) = self.public_key(context).await?
|
||||
&& let Some(relay_addrs) = addresses_from_public_key(&public_key)
|
||||
{
|
||||
Ok(Some(relay_addrs))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get name authorized by the contact.
|
||||
pub fn get_authname(&self) -> &str {
|
||||
&self.authname
|
||||
@@ -1968,8 +1957,8 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
|
||||
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
|
||||
fn cat_fingerprint(ret: &mut String, name: &str, fingerprint: &str) {
|
||||
*ret += &format!("\n\n{name}:\n{fingerprint}");
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
|
||||
@@ -3,6 +3,7 @@ use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
|
||||
use super::*;
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::key::secret_key_to_public_key;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
|
||||
@@ -887,11 +888,11 @@ async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
"Messages are end-to-end encrypted.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
Me:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
bob@example.net (bob@example.net):
|
||||
bob@example.net:
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487
|
||||
|
||||
@@ -908,11 +909,11 @@ bob@example.net"
|
||||
"No encryption.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
Me:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
bob@example.net (bob@example.net):
|
||||
bob@example.net:
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
@@ -1252,7 +1253,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
|
||||
let chat = bob.create_chat(alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
@@ -1348,7 +1349,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
async fn test_import_vcard_key_change() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_addr = &bob.get_primary_self_addr().await?;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
alice.evtracker.clear_events();
|
||||
@@ -1560,3 +1561,44 @@ async fn test_same_avatar_no_changed_event() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that relay list from imported key is preferred if the signature
|
||||
/// has the same timestamp as already imported key.
|
||||
/// If they key is signed twice with different relay list at the same second,
|
||||
/// we want to use the relay list from the message that arrives last
|
||||
/// so the newest relay list is used unless the messages are reordered.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_public_key_tiebreak() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let contact = bob.add_or_lookup_contact(alice).await;
|
||||
|
||||
let secret_key = test_utils::alice_keypair();
|
||||
let timestamp = pgp::types::Timestamp::now();
|
||||
|
||||
let addr = "alice@example.net";
|
||||
let relays1 = "alice@example.net,alice@example.org";
|
||||
let relays2 = "alice@example.net,alice@example.com";
|
||||
let relays1_vec: Vec<String> = relays1.split(',').map(String::from).collect();
|
||||
let relays2_vec: Vec<String> = relays2.split(',').map(String::from).collect();
|
||||
|
||||
let public_key1 =
|
||||
secret_key_to_public_key(secret_key.clone(), timestamp.as_secs(), addr, relays1)?;
|
||||
let public_key2 = secret_key_to_public_key(secret_key, timestamp.as_secs(), addr, relays2)?;
|
||||
|
||||
import_public_key(bob, &public_key1).await?;
|
||||
assert_eq!(contact.relay_addresses(bob).await?.unwrap(), relays1_vec);
|
||||
|
||||
import_public_key(bob, &public_key2).await?;
|
||||
assert_eq!(contact.relay_addresses(bob).await?.unwrap(), relays2_vec);
|
||||
|
||||
import_public_key(bob, &public_key1).await?;
|
||||
assert_eq!(contact.relay_addresses(bob).await?.unwrap(), relays1_vec);
|
||||
|
||||
import_public_key(bob, &public_key2).await?;
|
||||
assert_eq!(contact.relay_addresses(bob).await?.unwrap(), relays2_vec);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -949,12 +949,6 @@ impl Context {
|
||||
"gossip_period",
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"donation_request_next_check",
|
||||
self.get_config_i64(Config::DonationRequestNextCheck)
|
||||
|
||||
+77
-104
@@ -279,65 +279,20 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
{
|
||||
None => Timer::Disabled,
|
||||
Some(duration) => Timer::from_u32(duration),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time().saturating_add(duration.get().into());
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
(ephemeral_timestamp, ephemeral_timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
///
|
||||
/// Returns whether the timer was started.
|
||||
pub(crate) fn start_ephemeral_timer(self, tx: &rusqlite::Transaction) -> Result<bool> {
|
||||
let updated = tx.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer)
|
||||
AND ephemeral_timer > 0 AND id=?2",
|
||||
(time(), self),
|
||||
)?;
|
||||
Ok(updated > 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
let should_interrupt =
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let mut should_interrupt = false;
|
||||
let mut stmt =
|
||||
transaction.prepare(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ?1 + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?1 + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id=?2")?;
|
||||
for msg_id in msg_ids {
|
||||
should_interrupt |= stmt.execute((now, msg_id))? > 0;
|
||||
}
|
||||
Ok(should_interrupt)
|
||||
}).await?;
|
||||
if should_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts ephemeral timer for all messages in the chat.
|
||||
///
|
||||
/// This should be called when chat is marked as noticed.
|
||||
@@ -379,7 +334,7 @@ async fn select_expired_messages(
|
||||
SELECT id, chat_id, type, pinned, location_id
|
||||
FROM msgs
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
ephemeral_timestamp > 0
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
@@ -560,6 +515,14 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
|
||||
}
|
||||
}
|
||||
|
||||
/// `> 0` rather than `!= 0`, so that SQLite uses `msgs_index8`.
|
||||
const NEXT_EPHEMERAL_TIMESTAMP: &str = "
|
||||
SELECT min(ephemeral_timestamp)
|
||||
FROM msgs
|
||||
WHERE ephemeral_timestamp > 0
|
||||
AND chat_id != ?
|
||||
HAVING count(*) > 0";
|
||||
|
||||
/// Calculates next timestamp when expiration of some message will happen.
|
||||
///
|
||||
/// Expiration can happen either because user has set `delete_device_after` setting or because the
|
||||
@@ -568,13 +531,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
|
||||
let ephemeral_timestamp: Option<i64> = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
r#"
|
||||
SELECT min(ephemeral_timestamp)
|
||||
FROM msgs
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?
|
||||
HAVING count(*) > 0
|
||||
"#,
|
||||
NEXT_EPHEMERAL_TIMESTAMP,
|
||||
(ChatId::TRASH,), // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
@@ -661,6 +618,60 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
}
|
||||
}
|
||||
|
||||
/// Driven by `imap`: with `bcc_self` off and `force_encryption` on core deletes downloaded
|
||||
/// messages from the server and their `imap` rows with them, leaving rows only for messages
|
||||
/// it does not download.
|
||||
const MARK_DOWNLOADED_FOR_DELETION: &str = "
|
||||
UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1 AND target!=''
|
||||
AND (EXISTS (
|
||||
SELECT 1 FROM msgs
|
||||
WHERE rfc724_mid=imap.rfc724_mid
|
||||
AND id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR download_state=?3))
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM msgs
|
||||
WHERE pre_rfc724_mid=imap.rfc724_mid
|
||||
AND pre_rfc724_mid!=''
|
||||
AND id>9))";
|
||||
|
||||
/// Driven by `imap` like [`MARK_DOWNLOADED_FOR_DELETION`]: with `bcc_self` and
|
||||
/// `force_encryption` off core deletes only encrypted and expired messages from the server,
|
||||
/// so the rows of other messages remain.
|
||||
const MARK_ENCRYPTED_FOR_DELETION: &str = "
|
||||
UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1 AND target!=''
|
||||
AND (EXISTS (
|
||||
SELECT 1 FROM msgs
|
||||
WHERE rfc724_mid=imap.rfc724_mid
|
||||
AND id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?3)))
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM msgs
|
||||
WHERE pre_rfc724_mid=imap.rfc724_mid
|
||||
AND pre_rfc724_mid!=''
|
||||
AND id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
(param GLOB '*\nc=1*' OR param GLOB 'c=1*'))))";
|
||||
|
||||
/// Driven by `msgs_index8`: in multi-device mode core leaves downloaded messages on the server,
|
||||
/// so the `imap` table keeps a row for every message core has not deleted there.
|
||||
const MARK_EXPIRED_FOR_DELETION: &str = "
|
||||
UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1 AND target!=''
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ephemeral_timestamp>0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND ephemeral_timestamp>0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
)";
|
||||
|
||||
/// Schedules expired IMAP messages for deletion on the server.
|
||||
///
|
||||
/// Also see [`delete_expired_messages`],
|
||||
@@ -684,18 +695,7 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR download_state=?3)
|
||||
AND id>9
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND id>9
|
||||
)",
|
||||
MARK_DOWNLOADED_FOR_DELETION,
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
@@ -705,20 +705,7 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
// Only delete expired ephemeral messages.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2 AND id>9
|
||||
)",
|
||||
(transport_id, now),
|
||||
)
|
||||
.execute(MARK_EXPIRED_FOR_DELETION, (transport_id, now))
|
||||
.await?;
|
||||
} else {
|
||||
// Single device, but unencrypted messages are allowed.
|
||||
@@ -726,21 +713,7 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE transport_id=?1
|
||||
AND rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
((param GLOB '*\nc=1*' OR param GLOB 'c=1*') AND download_state=?3))
|
||||
UNION
|
||||
SELECT pre_rfc724_mid FROM msgs
|
||||
WHERE pre_rfc724_mid!=''
|
||||
AND id>9
|
||||
AND ((ephemeral_timestamp!=0 AND ephemeral_timestamp<=?2) OR
|
||||
(param GLOB '*\nc=1*' OR param GLOB 'c=1*'))
|
||||
)",
|
||||
MARK_ENCRYPTED_FOR_DELETION,
|
||||
(transport_id, now, DownloadState::Done),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -576,6 +576,31 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_expiration_queries_do_not_scan_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut scans = Vec::new();
|
||||
for (name, query) in [
|
||||
("NEXT_EPHEMERAL_TIMESTAMP", NEXT_EPHEMERAL_TIMESTAMP),
|
||||
("MARK_DOWNLOADED_FOR_DELETION", MARK_DOWNLOADED_FOR_DELETION),
|
||||
("MARK_EXPIRED_FOR_DELETION", MARK_EXPIRED_FOR_DELETION),
|
||||
("MARK_ENCRYPTED_FOR_DELETION", MARK_ENCRYPTED_FOR_DELETION),
|
||||
] {
|
||||
for step in t.query_plan(query).await? {
|
||||
// These statements run after every IMAP fetch or sent message. `SCAN msgs` reads
|
||||
// the whole table, and so does a rowid range from `id>9`, which SQLite plans as
|
||||
// `SEARCH msgs USING INTEGER PRIMARY KEY (rowid>?)`. Index lookups and ranges pass.
|
||||
if step.starts_with("SCAN msgs")
|
||||
|| step.ends_with("msgs USING INTEGER PRIMARY KEY (rowid>?)")
|
||||
{
|
||||
scans.push(format!("{name}: {step}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(scans.is_empty(), "{scans:#?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
|
||||
+66
-49
@@ -16,8 +16,7 @@ use std::{
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use async_imap::types::{Fetch, Flag, UnsolicitedResponse};
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use futures::TryStreamExt as _;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use url::Url;
|
||||
@@ -63,6 +62,20 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
)])";
|
||||
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
|
||||
|
||||
const PENDING_DELETIONS: &str = "
|
||||
SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND transport_id = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid";
|
||||
|
||||
/// `CROSS JOIN` loops over the few `imap_markseen` rows, not over the transport's `imap` rows.
|
||||
const PENDING_SEEN_FLAGS: &str = "
|
||||
SELECT imap.id, uid, folder FROM imap_markseen CROSS JOIN imap
|
||||
WHERE imap.id = imap_markseen.id
|
||||
AND imap.transport_id=?
|
||||
AND target = folder";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Imap {
|
||||
/// ID of the transport configuration in the `transports` table.
|
||||
@@ -274,17 +287,27 @@ impl Imap {
|
||||
"IMAP got rate limited, waiting for {} until can connect.",
|
||||
duration_to_str(ratelimit_duration),
|
||||
);
|
||||
let interrupted = async {
|
||||
tokio::time::sleep(ratelimit_duration).await;
|
||||
false
|
||||
}
|
||||
.race(self.idle_interrupt_receiver.recv().map(|_| true))
|
||||
.await;
|
||||
if interrupted {
|
||||
info!(
|
||||
context,
|
||||
"Connecting to IMAP without waiting for ratelimit due to interrupt."
|
||||
);
|
||||
match tokio::time::timeout(ratelimit_duration, self.idle_interrupt_receiver.recv())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
info!(
|
||||
context,
|
||||
"Connecting to IMAP without waiting for ratelimit due to interrupt."
|
||||
);
|
||||
}
|
||||
Ok(Err(async_channel::RecvError)) => {
|
||||
// If we get a RecvError, then this is likely because
|
||||
// the scheduler dropped the sender as it is stopping I/O.
|
||||
// Returning an error right away would make inbox_loop() call prepare() again.
|
||||
// Sleeping gives the executor a chance to cancel the loop;
|
||||
// the exact duration does not matter.
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
bail!("Not connecting to IMAP because interrupt channel is closed");
|
||||
}
|
||||
Err(_) => {
|
||||
// Rate limit duration has passed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +324,11 @@ impl Imap {
|
||||
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
|
||||
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
if login_params.is_empty() {
|
||||
// Count one connection attempt so we don't busy loop.
|
||||
self.ratelimit.send();
|
||||
}
|
||||
|
||||
let mut first_connection_error = None;
|
||||
let mut first_login_error = None;
|
||||
'candidate: for lp in login_params {
|
||||
@@ -453,8 +481,7 @@ impl Imap {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is
|
||||
// not well optimized and should not be called before fetching.
|
||||
// Mark messages for deletion on the server.
|
||||
delete_expired_imap_messages(context, session.transport_id())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
@@ -854,20 +881,12 @@ impl Session {
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, uid, target FROM imap
|
||||
WHERE folder = ?
|
||||
AND transport_id = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
(folder, transport_id),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
let target: String = row.get(2)?;
|
||||
Ok((rowid, uid, target))
|
||||
},
|
||||
)
|
||||
.query_map_vec(PENDING_DELETIONS, (folder, transport_id), |row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
let target: String = row.get(2)?;
|
||||
Ok((rowid, uid, target))
|
||||
})
|
||||
.await?;
|
||||
|
||||
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
@@ -916,19 +935,12 @@ impl Session {
|
||||
let transport_id = self.transport_id();
|
||||
let mut rows = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT imap.id, uid, folder FROM imap, imap_markseen
|
||||
WHERE imap.id = imap_markseen.id
|
||||
AND imap.transport_id=?
|
||||
AND target = folder",
|
||||
(transport_id,),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
let folder: String = row.get(2)?;
|
||||
Ok((rowid, uid, folder))
|
||||
},
|
||||
)
|
||||
.query_map_vec(PENDING_SEEN_FLAGS, (transport_id,), |row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
let folder: String = row.get(2)?;
|
||||
Ok((rowid, uid, folder))
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Number of SQL results is expected to be low as
|
||||
@@ -1579,19 +1591,24 @@ pub(crate) async fn prefetch_should_download(
|
||||
/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
|
||||
/// the given Message-ID to `imap_markseen` table.
|
||||
pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO imap_markseen (id)
|
||||
SELECT id FROM imap WHERE rfc724_mid=?",
|
||||
(message_id,),
|
||||
)
|
||||
let sql = &context.sql;
|
||||
sql.transaction(|tx| insert_imap_markseen(tx, message_id))
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Like [`markseen_on_imap_table`], but leaves interrupting the inbox loop to the caller.
|
||||
pub(crate) fn insert_imap_markseen(tx: &rusqlite::Transaction, message_id: &str) -> Result<()> {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO imap_markseen (id)
|
||||
SELECT id FROM imap WHERE rfc724_mid=?",
|
||||
(message_id,),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// uid_next is the next unique identifier value from the last time we fetched a folder
|
||||
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
|
||||
/// This function is used to update our uid_next after fetching messages.
|
||||
|
||||
@@ -116,3 +116,28 @@ fn test_uid_grouper() {
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_loop_queries_do_not_scan_imap() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut scans = Vec::new();
|
||||
for (name, query) in [
|
||||
("PENDING_DELETIONS", PENDING_DELETIONS),
|
||||
("PENDING_SEEN_FLAGS", PENDING_SEEN_FLAGS),
|
||||
] {
|
||||
for step in t.query_plan(query).await? {
|
||||
// A step on the `imap` table (not `imap_markseen`) may only look up single rows
|
||||
// by rowid or read the partial index of rows pending deletion. With any other index
|
||||
// SQLite walks at least every `imap` row of the transport.
|
||||
let reads_imap = step.contains(" imap ") || step.ends_with(" imap");
|
||||
if reads_imap
|
||||
&& !step.contains("USING INTEGER PRIMARY KEY (rowid=?)")
|
||||
&& !step.contains("USING INDEX imap_pending_deletions ")
|
||||
{
|
||||
scans.push(format!("{name}: {step}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(scans.is_empty(), "{scans:#?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -961,10 +961,6 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::ConfiguredAddr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1074,7 +1070,6 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert_eq!(context2.get_config(Config::ConfiguredAddr).await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+4
-14
@@ -124,15 +124,15 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
}
|
||||
|
||||
/// Converts secret key to public key.
|
||||
///
|
||||
/// `addr` is the address used in the User ID which is generated for compatibility.
|
||||
/// `relay_addrs` is the comma-separated list of relay addresses.
|
||||
pub(crate) fn secret_key_to_public_key(
|
||||
context: &Context,
|
||||
mut signed_secret_key: SignedSecretKey,
|
||||
timestamp: u32,
|
||||
addr: &str,
|
||||
relay_addrs: &str,
|
||||
) -> Result<SignedPublicKey> {
|
||||
info!(context, "Converting secret key to public key.");
|
||||
|
||||
// Make sure timestamp of created signatures
|
||||
// is not in the past compared to the primary key timestamp.
|
||||
let timestamp = std::cmp::max(
|
||||
@@ -305,7 +305,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let all_addrs = context.get_self_addrs().await?.join(",");
|
||||
let signed_public_key =
|
||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
secret_key_to_public_key(signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
*lock = Some(signed_public_key.clone());
|
||||
|
||||
Ok(Some(signed_public_key))
|
||||
@@ -660,7 +660,6 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
@@ -814,9 +813,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = load_self_public_key(&t).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
@@ -825,9 +821,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = load_self_secret_key(&t).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
@@ -838,9 +831,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let thr0 = {
|
||||
let ctx = t.clone();
|
||||
thread::spawn(move || {
|
||||
|
||||
+19
-7
@@ -188,13 +188,25 @@ pub(crate) async fn maybe_send_keyupdate_message(context: &Context) -> Result<()
|
||||
}
|
||||
|
||||
let recipients = keyupdate_recipients(context, KEYUPDATE_MAX_RECIPIENTS).await?;
|
||||
for chunk in recipients.chunks(KEYUPDATE_CHUNK_CONTACTS) {
|
||||
let envelope = envelope_recipients(chunk);
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let keys = chunk.iter().map(|r| r.public_key.clone()).collect();
|
||||
let queued_msg = keyupdate_message(context, &rfc724_mid, keys, envelope).await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_msg).await?;
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Sending keyupdate messages to {} recipients.",
|
||||
recipients.len()
|
||||
);
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| {
|
||||
for chunk in recipients.chunks(KEYUPDATE_CHUNK_CONTACTS) {
|
||||
let envelope = envelope_recipients(chunk);
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let keys = chunk.iter().map(|r| r.public_key.clone()).collect();
|
||||
let queued_msg = keyupdate_message(is_bot, &rfc724_mid, keys, envelope);
|
||||
insert_into_smtp(tx, &rfc724_mid, &queued_msg)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Record only after queueing, so failed queueing is retried by a later check.
|
||||
context
|
||||
|
||||
@@ -260,10 +260,6 @@ async fn test_keyupdate_on_replacement() -> Result<()> {
|
||||
alice.add_transport("alice@relay.example.net").await;
|
||||
SystemTime::shift(Duration::from_secs(2));
|
||||
alice.delete_transport("alice@example.org").await?;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
|
||||
Some("alice@relay.example.net")
|
||||
);
|
||||
|
||||
maybe_send_keyupdate_message(alice).await?;
|
||||
let keyupdate = alice.pop_sent_msg().await;
|
||||
|
||||
+101
-122
@@ -10,6 +10,7 @@ use deltachat_derive::{FromSql, ToSql};
|
||||
use humansize::BINARY;
|
||||
use humansize::format_size;
|
||||
use num_traits::FromPrimitive;
|
||||
use rusqlite::{OptionalExtension, Transaction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{fs, io};
|
||||
|
||||
@@ -22,9 +23,9 @@ use crate::contact::{self, Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::imap::insert_imap_markseen;
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
@@ -1777,87 +1778,69 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
|
||||
.await?;
|
||||
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for &id in &msg_ids {
|
||||
if let Some(msg) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
m.param AS param,
|
||||
m.from_id AS from_id,
|
||||
m.rfc724_mid AS rfc724_mid,
|
||||
m.hidden AS hidden,
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id=? AND m.chat_id>9",
|
||||
(id,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let hidden: bool = row.get("hidden")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
(
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
hidden,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
ephemeral_timer,
|
||||
))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
msgs.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if msgs
|
||||
.iter()
|
||||
.any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
|
||||
{
|
||||
start_ephemeral_timers_msgids(context, &msg_ids)
|
||||
.await
|
||||
.context("failed to start ephemeral timers")?;
|
||||
}
|
||||
|
||||
let should_send_mdns = context.should_send_mdns().await?;
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
let mut archived_chats_maybe_noticed = false;
|
||||
for (
|
||||
(
|
||||
id,
|
||||
curr_chat_id,
|
||||
curr_state,
|
||||
curr_param,
|
||||
curr_from_id,
|
||||
curr_rfc724_mid,
|
||||
curr_hidden,
|
||||
curr_visibility,
|
||||
curr_blocked,
|
||||
),
|
||||
_curr_ephemeral_timer,
|
||||
) in msgs
|
||||
{
|
||||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||||
update_msg_state(context, id, MessageState::InSeen).await?;
|
||||
let mut trigger_imap_interrupt = false;
|
||||
let mut trigger_smtp_interrupt = false;
|
||||
let mut trigger_ephemeral_interrupt = false;
|
||||
let sql = &context.sql;
|
||||
sql.transaction(|tx| {
|
||||
for id in msg_ids {
|
||||
let Some((
|
||||
curr_chat_id,
|
||||
curr_state,
|
||||
curr_param,
|
||||
curr_from_id,
|
||||
curr_rfc724_mid,
|
||||
curr_hidden,
|
||||
curr_visibility,
|
||||
curr_blocked,
|
||||
)) = tx
|
||||
.query_row(
|
||||
"SELECT
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.param AS param,
|
||||
m.from_id AS from_id,
|
||||
m.rfc724_mid AS rfc724_mid,
|
||||
m.hidden AS hidden,
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id=? AND m.chat_id>9",
|
||||
(id,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let param: Params =
|
||||
row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let hidden: bool = row.get("hidden")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let blocked = blocked.unwrap_or_default();
|
||||
Ok((
|
||||
chat_id, state, param, from_id, rfc724_mid, hidden, visibility,
|
||||
blocked,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
trigger_ephemeral_interrupt |= id.start_ephemeral_timer(tx)?;
|
||||
if !matches!(curr_state, MessageState::InFresh | MessageState::InNoticed) {
|
||||
continue;
|
||||
}
|
||||
update_msg_state(tx, id, MessageState::InSeen)?;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
|
||||
insert_imap_markseen(tx, &curr_rfc724_mid)?;
|
||||
trigger_imap_interrupt = true;
|
||||
|
||||
// Read receipts for system messages are never sent to contacts.
|
||||
// These messages have no place to display received read receipt
|
||||
@@ -1874,21 +1857,18 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
&& !curr_hidden
|
||||
&& wants_mdn
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
&& should_send_mdns
|
||||
{
|
||||
// Clear WantsMdn to not handle a MDN twice
|
||||
// if the state later is InFresh again as markfresh_chat() was called.
|
||||
// BccSelf MDN messages in the next branch may be sent twice for syncing.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?",
|
||||
(curr_param.clone().remove(Param::WantsMdn).to_string(), id),
|
||||
)
|
||||
.await
|
||||
.context("failed to clear WantsMdn")?;
|
||||
tx.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?",
|
||||
(curr_param.clone().remove(Param::WantsMdn).to_string(), id),
|
||||
)
|
||||
.context("failed to clear WantsMdn")?;
|
||||
Some(curr_from_id)
|
||||
} else if context.get_config_bool(Config::BccSelf).await? {
|
||||
} else if bcc_self {
|
||||
Some(ContactId::SELF)
|
||||
} else {
|
||||
None
|
||||
@@ -1899,24 +1879,32 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
"Queuing MDN to {to_id} for {id} from {curr_from_id}, wants_mdn={wants_mdn}, cmd={}.",
|
||||
curr_param.get_cmd()
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
(id, to_id, curr_rfc724_mid),
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
tx.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
(id, to_id, curr_rfc724_mid),
|
||||
)
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
trigger_smtp_interrupt = true;
|
||||
}
|
||||
|
||||
if !curr_hidden {
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
|
||||
&& !curr_hidden
|
||||
&& curr_visibility == ChatVisibility::Archived;
|
||||
}
|
||||
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
|
||||
&& !curr_hidden
|
||||
&& curr_visibility == ChatVisibility::Archived;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if trigger_imap_interrupt {
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
if trigger_smtp_interrupt {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
if trigger_ephemeral_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
@@ -1957,11 +1945,7 @@ pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Ve
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_msg_state(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
state: MessageState,
|
||||
) -> Result<()> {
|
||||
pub(crate) fn update_msg_state(tx: &Transaction, msg_id: MsgId, state: MessageState) -> Result<()> {
|
||||
ensure!(
|
||||
state != MessageState::OutMdnRcvd,
|
||||
"Update msgs_mdns table instead!"
|
||||
@@ -1971,13 +1955,10 @@ pub(crate) async fn update_msg_state(
|
||||
true => ", error=''",
|
||||
false => "",
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
|
||||
(state, msg_id),
|
||||
)
|
||||
.await?;
|
||||
tx.execute(
|
||||
&format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
|
||||
(state, msg_id),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2020,18 +2001,16 @@ pub(crate) async fn set_msg_failed(
|
||||
/// With `on_server`, copies seen on IMAP later are deleted on the server.
|
||||
///
|
||||
/// Returns tombstone database row ID.
|
||||
pub(crate) async fn insert_tombstone(
|
||||
context: &Context,
|
||||
pub(crate) fn insert_tombstone(
|
||||
tx: &mut rusqlite::Transaction<'_>,
|
||||
rfc724_mid: &str,
|
||||
on_server: bool,
|
||||
) -> Result<MsgId> {
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)
|
||||
.await?;
|
||||
tx.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
|
||||
(rfc724_mid, ChatId::TRASH, on_server),
|
||||
)?;
|
||||
let row_id = tx.last_insert_rowid();
|
||||
let msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
@@ -316,6 +316,24 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a read receipt failing to be queued is queued by the next `markseen_msgs()`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_msgs_failed_mdn_retried() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let msg = tcm.send_recv_accept(alice, bob, "hi").await;
|
||||
|
||||
let sql = &bob.sql;
|
||||
sql.execute("ALTER TABLE smtp_mdns RENAME TO t", ()).await?;
|
||||
assert!(markseen_msgs(bob, vec![msg.id]).await.is_err());
|
||||
sql.execute("ALTER TABLE t RENAME TO smtp_mdns", ()).await?;
|
||||
|
||||
markseen_msgs(bob, vec![msg.id]).await?;
|
||||
assert!(sql.exists("SELECT COUNT(*) FROM smtp_mdns", ()).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Message has been seen on another device when fully downloaded. `state` should be updated.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
|
||||
+27
-40
@@ -347,16 +347,15 @@ pub(crate) fn render_queued_mail(
|
||||
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
|
||||
continue;
|
||||
}
|
||||
let header_value =
|
||||
if header_name == "chat-version" || header_name == "chat-is-post-message" {
|
||||
parsed_header.get_value_raw()
|
||||
} else if header_name == "subject" {
|
||||
&b"[...]"[..]
|
||||
} else if header_name == "to" {
|
||||
&b"\"hidden-recipients\": ;"[..]
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let header_value = if header_name == "chat-is-post-message" {
|
||||
parsed_header.get_value_raw()
|
||||
} else if header_name == "subject" {
|
||||
&b"[...]"[..]
|
||||
} else if header_name == "to" {
|
||||
&b"\"hidden-recipients\": ;"[..]
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
outer_headers.extend(original_header_name.as_bytes());
|
||||
outer_headers.extend(b": ");
|
||||
@@ -433,9 +432,9 @@ pub(crate) fn render_queued_mail(
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn render_queued_mail_with_context(
|
||||
queued_mail: QueuedMail,
|
||||
context: &Context,
|
||||
context: &crate::test_utils::TestContext,
|
||||
) -> Result<RenderedEmail> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let from_addr = context.sending_addr().await;
|
||||
let public_key = crate::key::load_self_public_key(context).await?;
|
||||
let secret_key = crate::key::load_self_secret_key(context).await?;
|
||||
|
||||
@@ -999,14 +998,10 @@ impl MimeFactory {
|
||||
true => context.get_config(Config::Displayname).await?,
|
||||
false => None,
|
||||
};
|
||||
let self_name = &match self_name {
|
||||
Some(name) => name,
|
||||
None => context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Ok(stock_str::subject_for_new_contact(context, self_name))
|
||||
Ok(stock_str::subject_for_new_contact(
|
||||
context,
|
||||
self_name.as_deref().unwrap_or("..."),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn recipients(&self) -> Vec<String> {
|
||||
@@ -1228,7 +1223,7 @@ impl MimeFactory {
|
||||
///
|
||||
/// Used only for tests.
|
||||
#[cfg(test)]
|
||||
pub async fn render(self, context: &Context) -> Result<RenderedEmail> {
|
||||
pub async fn render(self, context: &crate::test_utils::TestContext) -> Result<RenderedEmail> {
|
||||
// Does not matter, we are not going to return the QueuedMail.
|
||||
let bcc_self = false;
|
||||
let (queued_mail, _side_effects) =
|
||||
@@ -2174,10 +2169,7 @@ fn b_encode(value: &str) -> String {
|
||||
|
||||
/// Returns the headers to place into the encrypted part
|
||||
/// of messages that are not part of a chat.
|
||||
async fn non_chat_headers(
|
||||
context: &Context,
|
||||
subject: &str,
|
||||
) -> Result<Vec<(&'static str, HeaderType<'static>)>> {
|
||||
fn non_chat_headers(is_bot: bool, subject: &str) -> Vec<(&'static str, HeaderType<'static>)> {
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(time(), 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
@@ -2187,10 +2179,10 @@ async fn non_chat_headers(
|
||||
("Subject", Text::new(subject.to_string()).into()),
|
||||
];
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
if is_bot {
|
||||
headers.push(("Auto-Submitted", Raw::new("auto-generated").into()));
|
||||
}
|
||||
Ok(headers)
|
||||
headers
|
||||
}
|
||||
|
||||
pub(crate) async fn symm_encrypted_securejoin_message(
|
||||
@@ -2206,7 +2198,8 @@ pub(crate) async fn symm_encrypted_securejoin_message(
|
||||
|
||||
let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join");
|
||||
|
||||
let mut headers = non_chat_headers(context, "Secure-Join").await?;
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
let mut headers = non_chat_headers(is_bot, "Secure-Join");
|
||||
headers.push(("Secure-Join", Raw::new(step.to_string()).into()));
|
||||
headers.push(("Secure-Join-Auth", Text::new(auth.to_string()).into()));
|
||||
|
||||
@@ -2384,23 +2377,18 @@ fn keyupdate_body() -> MimePart<'static> {
|
||||
|
||||
/// Returns a keyupdate message informing the owners of `recipient_keys`
|
||||
/// about the current key and relay list, see [`crate::keyupdate`].
|
||||
pub(crate) async fn keyupdate_message(
|
||||
context: &Context,
|
||||
pub(crate) fn keyupdate_message(
|
||||
is_bot: bool,
|
||||
rfc724_mid: &str,
|
||||
recipient_keys: Vec<SignedPublicKey>,
|
||||
recipients: Vec<String>,
|
||||
) -> Result<QueuedMail> {
|
||||
info!(
|
||||
context,
|
||||
"Sending keyupdate message to {} recipients.",
|
||||
recipient_keys.len()
|
||||
);
|
||||
) -> QueuedMail {
|
||||
let message = keyupdate_body();
|
||||
|
||||
let headers = non_chat_headers(context, "Keyupdate").await?;
|
||||
let headers = non_chat_headers(is_bot, "Keyupdate");
|
||||
let message = add_headers_to_encrypted_part(message, headers);
|
||||
|
||||
let queued_mail = QueuedMail {
|
||||
QueuedMail {
|
||||
raw_message: part_to_bytes(message),
|
||||
display_name: String::new(),
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
@@ -2420,8 +2408,7 @@ pub(crate) async fn keyupdate_message(
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
bcc_self: false,
|
||||
};
|
||||
Ok(queued_mail)
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders MIME part into a vector of bytes.
|
||||
|
||||
@@ -185,7 +185,7 @@ async fn test_subject_outgoing() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
|
||||
assert_eq!(first_subject_str(t).await, "Message from ...");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.allow_unencrypted().await.unwrap();
|
||||
@@ -362,7 +362,6 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
|
||||
// Bob's key gets a second relay address and Alice merges the newer key.
|
||||
let bob_secret_key = load_self_secret_key(bob).await?;
|
||||
let bob_public_key = secret_key_to_public_key(
|
||||
bob,
|
||||
bob_secret_key,
|
||||
u32::try_from(time())? + 100,
|
||||
"bob@example.net",
|
||||
@@ -732,9 +731,9 @@ async fn test_remove_member_bcc() -> Result<()> {
|
||||
let charlie = &tcm.charlie().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let alice_addr = alice.get_primary_self_addr().await?;
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let charlie_addr = charlie.get_primary_self_addr().await?;
|
||||
|
||||
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
@@ -897,7 +896,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&charlie.get_config(Config::ConfiguredAddr).await?.unwrap())
|
||||
.starts_with(&charlie.get_primary_self_addr().await?)
|
||||
);
|
||||
|
||||
remove_contact_from_chat(alice, group, bob_id).await?;
|
||||
@@ -905,11 +904,8 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, group, bob_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&bob.get_config(Config::ConfiguredAddr).await?.unwrap())
|
||||
);
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
assert!(sent_msg.recipients.starts_with(&bob_addr));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1059,7 +1055,6 @@ Message-ID: <MESSAGE_ID@localhost>
|
||||
MIME-Version: 1.0
|
||||
To: "hidden-recipients": ;
|
||||
Subject: [...]
|
||||
Chat-Version: 1.0
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="BOUNDARY"
|
||||
|
||||
@@ -1117,7 +1112,7 @@ Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
Subject: Message from ...
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
@@ -1169,7 +1164,7 @@ Content-Type: multipart/mixed;
|
||||
boundary="BOUNDARY"
|
||||
Date: DATE
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
Subject: Message from ...
|
||||
References: <MESSAGE_ID@localhost>
|
||||
Chat-Version: 1.0
|
||||
|
||||
|
||||
+23
-24
@@ -701,7 +701,6 @@ impl MimeMessage {
|
||||
Ok(parser)
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn get_timestamp_sent(
|
||||
hdrs: &[mailparse::MailHeader<'_>],
|
||||
default: i64,
|
||||
@@ -710,7 +709,10 @@ impl MimeMessage {
|
||||
hdrs.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.map_or(default, |value| {
|
||||
min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
|
||||
min(
|
||||
value,
|
||||
timestamp_rcvd.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -970,7 +972,6 @@ impl MimeMessage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn avatar_action_from_header(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -990,23 +991,19 @@ impl MimeMessage {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if let Some(i) = self
|
||||
.parts
|
||||
.iter()
|
||||
.position(|part| part.org_filename.as_ref() == Some(&header_value))
|
||||
{
|
||||
// Previous versions of Delta Chat sent avatars as attachments
|
||||
// with the filename of the attachment sent in the Chat-User-Avatar header.
|
||||
self.parts
|
||||
.remove(i)
|
||||
.param
|
||||
.get(Param::File)
|
||||
.map(|blob| AvatarAction::Change(blob.to_string()))
|
||||
} else {
|
||||
// Avatar sent in attachment, as previous versions of Delta Chat did.
|
||||
|
||||
let mut i = 0;
|
||||
while let Some(part) = self.parts.get_mut(i) {
|
||||
if let Some(part_filename) = &part.org_filename
|
||||
&& part_filename == &header_value
|
||||
{
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
let res = Some(AvatarAction::Change(blob.to_string()));
|
||||
self.parts.remove(i);
|
||||
return Ok(res);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
};
|
||||
Ok(res)
|
||||
@@ -1367,7 +1364,7 @@ impl MimeMessage {
|
||||
self.do_add_single_part(part);
|
||||
return Ok(true);
|
||||
}
|
||||
mime::TEXT | mime::HTML => {
|
||||
mime::TEXT => {
|
||||
let decoded_data = match mail.get_body() {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(err) => {
|
||||
@@ -1490,7 +1487,6 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn do_add_single_file_part(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1565,7 +1561,11 @@ impl MimeMessage {
|
||||
{
|
||||
match get_filemeta(decoded_data) {
|
||||
// image size is known, not too big, keep msg_type:
|
||||
Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
|
||||
Ok((width, height))
|
||||
if width
|
||||
.checked_mul(height)
|
||||
.is_some_and(|pixels| pixels <= constants::MAX_RCVD_IMAGE_PIXELS) =>
|
||||
{
|
||||
part.param.set_i64(Param::Width, width.into());
|
||||
part.param.set_i64(Param::Height, height.into());
|
||||
msg_type
|
||||
@@ -2045,9 +2045,8 @@ impl MimeMessage {
|
||||
/// Returns parsed `Chat-Group-Member-Timestamps` header contents.
|
||||
///
|
||||
/// Returns `None` if there is no such header.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
|
||||
let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
|
||||
let now = time().saturating_add(constants::TIMESTAMP_SENT_TOLERANCE);
|
||||
self.get_header(HeaderDef::ChatGroupMemberTimestamps)
|
||||
.map(|h| {
|
||||
h.split_ascii_whitespace()
|
||||
|
||||
@@ -2228,3 +2228,27 @@ async fn test_load_shared_secrets_with_legacy_state() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that MIME parts with html/* MIME type are ignored.
|
||||
///
|
||||
/// HTML parts should have a text/html MIME type.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ignore_html_mime_parts() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let encrypted_msg = test_utils::encrypt_raw_message(
|
||||
bob,
|
||||
&[alice],
|
||||
include_bytes!("../../test-data/message/html_html_mime.eml"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = MimeMessage::from_bytes(alice, encrypted_msg.as_bytes()).await?;
|
||||
assert_eq!(msg.parts.len(), 1);
|
||||
assert_eq!(msg.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(msg.parts[0].mimetype, Some(mime::TEXT_PLAIN));
|
||||
assert_eq!(msg.parts[0].msg, "Here is a text part.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ async fn test_broadcast_security_attacker_signature() -> Result<()> {
|
||||
|
||||
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
|
||||
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_primary_self_addr().await?;
|
||||
|
||||
test_shared_secret_decryption_ext(
|
||||
bob,
|
||||
@@ -183,7 +183,7 @@ async fn test_broadcast_security_happy_path() -> Result<()> {
|
||||
|
||||
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
|
||||
|
||||
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let alice_addr = alice.get_primary_self_addr().await?;
|
||||
|
||||
test_shared_secret_decryption_ext(bob, &alice_addr, &secret, Some(alice), None).await
|
||||
}
|
||||
@@ -202,7 +202,7 @@ async fn test_qr_code_security() -> Result<()> {
|
||||
// Start a securejoin process, but don't finish it:
|
||||
join_securejoin(bob, &qr).await?;
|
||||
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_primary_self_addr().await?;
|
||||
|
||||
let alice_fp = self_fingerprint(alice).await?;
|
||||
let secret_for_encryption = format!("securejoin/{alice_fp}/{authcode}");
|
||||
|
||||
+29
-16
@@ -316,10 +316,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"mail.buzon.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
|
||||
),
|
||||
(
|
||||
"daleth.cafe",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
|
||||
),
|
||||
(
|
||||
"disroot.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
|
||||
@@ -680,6 +676,7 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
],
|
||||
),
|
||||
// Known public chatmail relays from https://chatmail.at/relays
|
||||
// and default onboarding relays.
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
@@ -688,6 +685,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"mailchat.pl",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(46, 62, 144, 137))],
|
||||
),
|
||||
(
|
||||
"nchrcht.la10cy.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(217, 197, 86, 157))],
|
||||
),
|
||||
(
|
||||
"chatmail.woodpeckersnest.space",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(85, 215, 162, 146))],
|
||||
@@ -704,6 +705,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"chat.adminforge.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(94, 130, 17, 142))],
|
||||
),
|
||||
(
|
||||
"chat.me.ke",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(169, 255, 58, 122))],
|
||||
),
|
||||
(
|
||||
"chika.aangat.lahat.computer",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(71, 19, 150, 113))],
|
||||
@@ -725,8 +730,20 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
vec![IpAddr::V4(Ipv4Addr::new(45, 124, 54, 79))],
|
||||
),
|
||||
(
|
||||
"sombras.chat",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(82, 25, 70, 154))],
|
||||
"chatmail.cc",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(15, 235, 198, 20))],
|
||||
),
|
||||
(
|
||||
"deltachat.me",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(102, 68, 86, 134))],
|
||||
),
|
||||
(
|
||||
"e2e.sus.fr",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(93, 95, 231, 235))],
|
||||
),
|
||||
(
|
||||
"chatmail.uk",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 165, 89, 146))],
|
||||
),
|
||||
(
|
||||
"e2ee.wang",
|
||||
@@ -736,7 +753,7 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"chat.privittytech.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(35, 154, 144, 0))],
|
||||
),
|
||||
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 57))]),
|
||||
("e2ee.im", vec![IpAddr::V4(Ipv4Addr::new(45, 137, 99, 156))]),
|
||||
(
|
||||
"chatmail.email",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(57, 128, 220, 120))],
|
||||
@@ -753,6 +770,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"chat.nuvon.app",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 238, 38, 165))],
|
||||
),
|
||||
(
|
||||
"chat.tinydispatch.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(88, 198, 181, 62))],
|
||||
),
|
||||
(
|
||||
"nibblehole.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(94, 247, 42, 209))],
|
||||
@@ -761,10 +782,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
"chat.zashm.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(91, 245, 76, 39))],
|
||||
),
|
||||
(
|
||||
"chat.sus.fr",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(152, 67, 76, 190))],
|
||||
),
|
||||
(
|
||||
"delta.thelab.uno",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(146, 59, 228, 39))],
|
||||
@@ -779,16 +796,12 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
|
||||
),
|
||||
(
|
||||
"sweetfern.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 156, 228, 133))],
|
||||
vec![IpAddr::V4(Ipv4Addr::new(51, 81, 78, 167))],
|
||||
),
|
||||
(
|
||||
"delta.disobey.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(37, 74, 102, 44))],
|
||||
),
|
||||
(
|
||||
"darkrun.dev",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(72, 11, 149, 146))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
|
||||
+1
-21
@@ -23,7 +23,7 @@
|
||||
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
|
||||
//! and `joinRealtimeChannel().send()` just like the other peers.
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
|
||||
@@ -38,7 +38,6 @@ use url::Url;
|
||||
|
||||
use crate::EventType;
|
||||
use crate::chat::send_msg;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
@@ -299,10 +298,6 @@ impl Context {
|
||||
pub async fn get_or_try_init_peer_channel(
|
||||
&self,
|
||||
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
|
||||
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
bail!("Attempt to initialize Iroh when realtime is disabled");
|
||||
}
|
||||
|
||||
if let Some(lock) = self.get_peer_channels().await {
|
||||
return Ok(lock);
|
||||
}
|
||||
@@ -360,13 +355,6 @@ pub async fn add_gossip_peer_from_header(
|
||||
instance_id: MsgId,
|
||||
node_addr: &str,
|
||||
) -> Result<()> {
|
||||
if !context
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
@@ -464,10 +452,6 @@ pub async fn send_webxdc_realtime_advertisement(
|
||||
ctx: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<Option<oneshot::Receiver<()>>> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
let conn = iroh.join_and_subscribe_gossip(ctx, msg_id).await?;
|
||||
|
||||
@@ -483,10 +467,6 @@ pub async fn send_webxdc_realtime_advertisement(
|
||||
|
||||
/// Send realtime data to other peers using iroh.
|
||||
pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u8>) -> Result<()> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
iroh.send_webxdc_realtime_data(ctx, msg_id, data).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -242,12 +242,6 @@ async fn test_can_reconnect() {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
assert!(
|
||||
alice
|
||||
.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
@@ -588,49 +582,11 @@ async fn connect_alice_bob(
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_peer_channels_disabled() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_advertisement(alice, MsgId::new(1))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
// creates iroh endpoint as side effect
|
||||
send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
// This internal function should return error
|
||||
// if accidentally called with the setting disabled.
|
||||
assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_webxdc_realtime_uninitialized() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
|
||||
assert!(alice.ctx.iroh.read().await.is_none());
|
||||
|
||||
+68
-9
@@ -1,5 +1,6 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -83,13 +84,64 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
|
||||
|
||||
/// Selects a subkey of the public key to use for encryption.
|
||||
///
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
/// The key is selected according to
|
||||
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
|
||||
/// If multiple keys are available, the one that will expire sooner is selected.
|
||||
///
|
||||
/// TODO: take key flags and expiration dates into account
|
||||
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
key.public_subkeys
|
||||
.iter()
|
||||
.find(|subkey| subkey.algorithm().can_encrypt())
|
||||
.filter(|subkey| subkey.algorithm().can_encrypt())
|
||||
.filter_map(|subkey| {
|
||||
// We take arbitrary signature here and don't try to select the newest one.
|
||||
// Filtering out outdated and invalid signatures should happen during certificate merging.
|
||||
let signature = subkey.signatures.first()?;
|
||||
|
||||
let key_flags = signature.key_flags();
|
||||
if !key_flags.encrypt_comms() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(expiration_duration) = signature
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0)
|
||||
&& now
|
||||
> subkey
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration_duration.as_secs())
|
||||
{
|
||||
// Key is expired.
|
||||
return None;
|
||||
}
|
||||
Some((subkey, signature))
|
||||
})
|
||||
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
|
||||
match (
|
||||
signature1
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
signature2
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(Some(expiration1), Some(expiration2)) => (subkey1
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration1.as_secs()))
|
||||
.cmp(
|
||||
&(subkey2
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration2.as_secs())),
|
||||
),
|
||||
}
|
||||
})
|
||||
.map(|(subkey, _signature)| subkey)
|
||||
}
|
||||
|
||||
/// Version of SEIPD packet to use.
|
||||
@@ -110,12 +162,13 @@ pub enum SeipdVersion {
|
||||
/// encrypted to `public_keys_for_encryption`.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
fn signature_subpackets(
|
||||
now: pgp::types::Timestamp,
|
||||
private_key_for_signing: &SignedSecretKey,
|
||||
public_keys_for_encryption: &[SignedPublicKey],
|
||||
) -> Result<SubpacketConfig> {
|
||||
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
|
||||
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
pgp::types::Timestamp::now(),
|
||||
now,
|
||||
))?);
|
||||
for key in public_keys_for_encryption {
|
||||
let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint());
|
||||
@@ -151,10 +204,11 @@ pub fn pk_encrypt(
|
||||
) -> Result<String> {
|
||||
tokio::task::block_in_place(|| {
|
||||
let mut rng = thread_rng();
|
||||
let now = pgp::types::Timestamp::now();
|
||||
|
||||
let pkeys = public_keys_for_encryption
|
||||
.iter()
|
||||
.filter_map(select_pk_for_encryption);
|
||||
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
|
||||
|
||||
let msg = MessageBuilder::from_bytes("", plain);
|
||||
let encoded_msg = match seipd_version {
|
||||
@@ -166,7 +220,8 @@ pub fn pk_encrypt(
|
||||
}
|
||||
|
||||
if let Some(secret_key) = private_key_for_signing {
|
||||
let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?;
|
||||
let subpkts =
|
||||
signature_subpackets(now, secret_key, &public_keys_for_encryption)?;
|
||||
let hash_algorithm = secret_key.hash_alg();
|
||||
msg.sign_with_subpackets(
|
||||
&**secret_key,
|
||||
@@ -194,7 +249,8 @@ pub fn pk_encrypt(
|
||||
}
|
||||
|
||||
if let Some(secret_key) = private_key_for_signing {
|
||||
let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?;
|
||||
let subpkts =
|
||||
signature_subpackets(now, secret_key, &public_keys_for_encryption)?;
|
||||
let hash_algorithm = secret_key.hash_alg();
|
||||
msg.sign_with_subpackets(
|
||||
&**secret_key,
|
||||
@@ -326,6 +382,8 @@ pub fn symm_encrypt_message(
|
||||
/// or even have some packets maliciously dropped
|
||||
/// (for example, all encryption subkeys dropped)
|
||||
/// or restored from some older version of the certificate.
|
||||
///
|
||||
/// In case of ties, i.e. same timestamps, data from the new certificate is preferred.
|
||||
pub fn merge_openpgp_certificates(
|
||||
old_certificate: SignedPublicKey,
|
||||
new_certificate: SignedPublicKey,
|
||||
@@ -485,7 +543,8 @@ pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<Strin
|
||||
|
||||
/// Returns true if the key can be encrypted to, i.e. has an encryption subkey.
|
||||
pub(crate) fn pubkey_can_encrypt(public_key: &SignedPublicKey) -> bool {
|
||||
select_pk_for_encryption(public_key).is_some()
|
||||
let now = pgp::types::Timestamp::now();
|
||||
select_pk_for_encryption(now.as_secs(), public_key).is_some()
|
||||
}
|
||||
|
||||
/// Returns true if public key advertises SEIPDv2 feature.
|
||||
|
||||
+236
-4
@@ -2,15 +2,16 @@ use std::sync::LazyLock;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::key::secret_key_to_public_key;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
use crate::{
|
||||
config::Config,
|
||||
decrypt,
|
||||
key::{load_self_public_key, self_fingerprint, store_self_keypair},
|
||||
mimefactory::{part_to_bytes, wrap_encrypted_part},
|
||||
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair},
|
||||
token,
|
||||
};
|
||||
use chrono::{TimeZone as _, Utc};
|
||||
use pgp::composed::{Esk, Message};
|
||||
use pgp::packet::PublicKeyEncryptedSessionKey;
|
||||
|
||||
@@ -23,9 +24,6 @@ async fn decrypt_bytes(
|
||||
add_pseudo_transport(t, "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to add pseudo transport");
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.expect("Failed to configure address");
|
||||
|
||||
for secret in auth_tokens_for_decryption {
|
||||
token::save(t, token::Namespace::Auth, None, secret, 0).await?;
|
||||
@@ -387,6 +385,31 @@ fn test_merge_openpgp_certificates() {
|
||||
assert!(merge_openpgp_certificates(bob, alice).is_err());
|
||||
}
|
||||
|
||||
/// Tests that Direct Key Signature from the new certificate is preferred
|
||||
/// when the timestamp is the same.
|
||||
#[test]
|
||||
fn test_merge_openpgp_certificates_tiebreak() -> Result<()> {
|
||||
let secret_key = alice_keypair();
|
||||
let timestamp = pgp::types::Timestamp::now();
|
||||
|
||||
let addr = "alice@example.net";
|
||||
let relays1 = "alice@example.net,alice@example.org";
|
||||
let relays2 = "alice@example.net,alice@example.com";
|
||||
|
||||
let public_key1 =
|
||||
secret_key_to_public_key(secret_key.clone(), timestamp.as_secs(), addr, relays1)?;
|
||||
let public_key2 = secret_key_to_public_key(secret_key, timestamp.as_secs(), addr, relays2)?;
|
||||
|
||||
let merge12 = merge_openpgp_certificates(public_key1.clone(), public_key2.clone())?;
|
||||
let merge21 = merge_openpgp_certificates(public_key2.clone(), public_key1.clone())?;
|
||||
|
||||
assert_eq!(merge12, public_key2);
|
||||
assert_eq!(merge21, public_key1);
|
||||
assert_ne!(merge12, merge21);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test PQC support.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pqc() -> Result<()> {
|
||||
@@ -423,3 +446,212 @@ async fn test_securejoin_pqc_joiner() {
|
||||
|
||||
tcm.execute_securejoin(bob, pqc).await;
|
||||
}
|
||||
|
||||
/// Tests that public subkey selection for encryption respects key flags and prefers expring subkeys.
|
||||
///
|
||||
/// Non-encryption subkeys such as RSA subkey for authentication are ignored.
|
||||
#[test]
|
||||
fn test_select_pk_for_encryption() {
|
||||
// Public key generated with GnuPG 2.4.9 with the following subkeys:
|
||||
// 1. Auth-only RSA subkey (92E762B9084CA740).
|
||||
// 2. Expired Curve25519 encryption subkey with 1-day expiration (C8F382BD0F35C49E)
|
||||
// 3. Ed25519 signing subkey (F177AC3118F923CC).
|
||||
// 4. Curve25519 encryption subkey with fingerprint (36188C6FFC8E267B)
|
||||
// 5. Curve25519 encryption subkey with 1 year expiration, valid in the beginning of 2008, with key ID 9223FCEE7546CDE7
|
||||
// 6. Curve25519 encryption subkey with no expiration (FD2C0567967223D8).
|
||||
// Primary key is an Ed25519 not expiring key.
|
||||
// Key 4 is the one that should be selected.
|
||||
|
||||
// Subkey 4 fingerprint.
|
||||
let expected_fallback_fingerprint = "cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b";
|
||||
|
||||
// Subkey 5 fingerprint, should be preferred to fallback when not expired.
|
||||
let expected_expiring_fingerprint = "5133fab157c4a46ca41f6dc39223fcee7546cde7";
|
||||
|
||||
let alice_tpk_asc = "
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMERvfcPBYJKwYBBAHaRw8BAQdAimvPsr7NdJ4dBoFPySwhpTQqoYOoHL3AzfE7
|
||||
mGWQOOC0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBCi19Yqv
|
||||
ugVVkhyHgicqAms0FFoiBQJG99w8AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
|
||||
AAoJECcqAms0FFoiUGEA/3VZMBCoRq0ZpHarzmvzgdZCoL3r3m9en/eZScFzxITx
|
||||
AP42Mn27r0SOKwIln0VcPTdAQCk49mBW/EX3CMOlLPU3DLkBjQRG99w8AQwArsYe
|
||||
Jkdl6sSM/hfoEw0vgx/RdUBQ6QRYi1uc0UUNlIGy8mlczLFdkD3JF/hGocjPvt45
|
||||
XAQoK110zAkZlfpFRqNT1M/IC68Er8rLkYPC4OeFh6W4Iyn17fcUanP0lf8em/jh
|
||||
Vffvgy8sFOMdO235lvFA3txNA98s4fHdmU3PScyd1hc3C4M0yP83LnYyWt4X59Xc
|
||||
E/Om5Dm458eKCSeYkLI6752W0mXsBxSi3/dLn0XeuNRpgmKxSkm562FHOFaLbKtR
|
||||
Y5hobAI9PkNcVgRxZZvWQls5PHTZWjqngF21lKlaLfdqZ/Uae1i2hzZOihgitL43
|
||||
Le00qwNBi5hKYMeuDnHaQrLmb+A+0/IAEE4Ub+TkZhzI+2ZP2k1cAT0qZmZi0w+q
|
||||
xqP9INk0hY/oZFCnV2wkHN7zvQmVlUIcQ2rmfbafK1yiEL1qeGT96zyjbdXeGPqJ
|
||||
3O9h+YIG38JMRJBijsFujUN34Z546zS/kzOPXsz/WlGUMjwu8n5s5uf2TFyFABEB
|
||||
AAGIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsgAAoJECcq
|
||||
Ams0FFoiCOUBAPafRLDpWN9iT4hcCXjESf1Hw5KNVkJpwfzPfu2H9BkMAQCaHhKg
|
||||
pq9ywH4pyOHZCPV8P2ywkyn+EsjBC3fG+GBBBbg4BEb33DwSCisGAQQBl1UBBQEB
|
||||
B0DfI8AJFT3nWa6ZXLkHSf7W8W7S6AWIO7LAcjoyHwb8CwMBCAeIfgQYFgoAJhYh
|
||||
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMBQkAAVGAAAoJECcqAms0FFoi
|
||||
U5EA/3G74HRwIMJlNOEW5gkYYV5KJW2qgtMfxHCUjoHvNWU1AQCHt/bLU2aviAiS
|
||||
of1R43qojxKUqzzoi8lYRQ+1sYhvB7gzBEb33DwWCSsGAQQB2kcPAQEHQKZXUJ7s
|
||||
xqH3kVcMnhasw6DrFMwCxHDdj+qvkg8r/DvtiO8EGBYKACAWIQQotfWKr7oFVZIc
|
||||
h4InKgJrNBRaIgUCRvfcPAIbAgCBCRAnKgJrNBRaInYgBBkWCgAdFiEEI8hstnVQ
|
||||
9sgylIYg8XesMRj5I8wFAkb33DwACgkQ8XesMRj5I8xo6QEAu4o/TyEZwFcyqZpw
|
||||
LEo9vTLCsc7fo0nx0ssiP6FyV5cBAOWal1DznDhsXWCNt+U8UaafXsU2DTV51KaD
|
||||
VBOVFo4CyeQBAMirIjXV5PbUV674TNLhYl2s0jTtNz+GKtOjSdZuRm1mAPwPG6ya
|
||||
K1b7iMRdBT92gNZMw30LbtcXmttCxpZAwr9lBLg4BEb33DwSCisGAQQBl1UBBQEB
|
||||
B0C5fFb4WTHoIoI6ou/31+1N1wn8ghsSkUVzpbtv/aTkegMBCAeIeAQYFgoAIBYh
|
||||
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMAAoJECcqAms0FFoi8z8BALPL
|
||||
7V0ICLEY5YSUa4lQ2rjiXOcVTlWkG3h4TATPrr08AP9tIAQIE0o50IGdQAcKJoTn
|
||||
Lyxnf2wfjZ16vL3JLLjSBrg4BEb33DwSCisGAQQBl1UBBQEBB0DXDrcGrnuLjAUO
|
||||
eo/t8MQNOe+ZKYSDPGTkO7iM5IloSAMBCAeIfgQYFgoAJhYhBCi19YqvugVVkhyH
|
||||
gicqAms0FFoiBQJG99w8AhsMBQkB4TOAAAoJECcqAms0FFoivQIA/2PcZ1vcImAa
|
||||
7ldPY00JkcW6WlSSd6yOIZsVa4TdA1FiAP42gOji+4RrLps2+NX6L1znSc8EJBXo
|
||||
RMbND/CZQWXfA7g4BEb33DwSCisGAQQBl1UBBQEBB0BHxCvo5zuygw2XiluYNobx
|
||||
7iFJqlmCkjekKyoVFquKHQMBCAeIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0
|
||||
FFoiBQJG99w8AhsMAAoJECcqAms0FFoirSMA/0gVP98sPFga+UhQ3uJxJw5bO2Rs
|
||||
7hxVk6aPREWgBYg1AQCT7AE8m7j17SP/1fl8OjpxsQQmCJyv2wNcP48OfKGOCA==
|
||||
=AXAi
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
";
|
||||
|
||||
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
|
||||
let now = pgp::types::Timestamp::now();
|
||||
let encryption_subkey = select_pk_for_encryption(now.as_secs(), &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
encryption_subkey.fingerprint().to_string().as_str(),
|
||||
expected_fallback_fingerprint
|
||||
);
|
||||
|
||||
// If we pass 1199149200 as the `now` argument, which was in the beginning of 2008,
|
||||
// then the expiring subkey is not yet expired and should be used.
|
||||
let encryption_subkey = select_pk_for_encryption(1199149200, &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
encryption_subkey.fingerprint().to_string().as_str(),
|
||||
expected_expiring_fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that key selection fails if there is only an expired subkey.
|
||||
#[test]
|
||||
fn test_select_only_expired_subkey() {
|
||||
let alice_tpk_asc = "
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMERvfcPBYJKwYBBAHaRw8BAQdAUz6AZXIRE8T04Vh8RReFiP3tEV8UfSs2EiYs
|
||||
8b8i4ce0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBNRG3w/A
|
||||
qWyPitBaqkGLyqDx3CaUBQJG99w8AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
|
||||
AAoJEEGLyqDx3CaUMjIA/Rq9/iORLP360s6EsIDe9qSmlmSCggtivafH+uVBWa0X
|
||||
AP0Wb+kailDwISq2O9Ef/jceZw7ozyzDLeDskKpCYiL3A7g4BEb33DwSCisGAQQB
|
||||
l1UBBQEBB0BXhbrks9iskW1GfT3B022W3KhJCgz8gw81z9lAWv7OZgMBCAeIfgQY
|
||||
FgoAJhYhBNRG3w/AqWyPitBaqkGLyqDx3CaUBQJG99w8AhsMBQkB4TOAAAoJEEGL
|
||||
yqDx3CaU6/YA/jaYJsZMXvu5grrMq1wq3Z/yzGXd15zeWp+alY/6UYxoAQCmSrBC
|
||||
SOtE3ODLZN9tC3F7k1N9clme1cHXyUiH3EliBQ==
|
||||
=zWPq
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
";
|
||||
|
||||
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
|
||||
let now = pgp::types::Timestamp::now();
|
||||
|
||||
assert_eq!(select_pk_for_encryption(now.as_secs(), &alice_tpk), None);
|
||||
}
|
||||
|
||||
/// Tests that the key with the closest expiration date is selected.
|
||||
#[test]
|
||||
fn test_expiring_subkey_selection() {
|
||||
// The key has the following keys:
|
||||
// 1. Primary non-expiring Ed25519 key created on 2025-01-01
|
||||
// 2. Non-expiring subkey CE5FC3FD479E41F08069DCE6F6CAD4ADB9AF47F1 created on 2025-01-01
|
||||
// 3. Subkey 10493EF4DBA7EE4D55C826A7634684D9BFEEB890 created on 2026-06-01 and expiring on 2026-07-01
|
||||
// 4. Subkey 477650C13FA4842BAF2831BC41C2D560BE27B38D created on 2026-06-01 and expiring on 2026-06-15
|
||||
// 5. Subkey AB92F4AFD46DDD7D31FB05151BE08A8089F67CA2 created on 2026-06-08 and expiring on 2026-06-22
|
||||
let alice_tpk_asc = "
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZ3SFgBYJKwYBBAHaRw8BAQdAOrjYsxkYNvVtjbZwvglXk94Rd8S3F0YlIgcQ
|
||||
SSp45ni0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBGV3iWZU
|
||||
bA6WJh3MR00wvB0X57PiBQJndIWAAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
|
||||
AAoJEE0wvB0X57PiAr0A/0qzulPRXZR1+D1fUeW6/C3BRP+8qRZTlOvF0XEnxPOV
|
||||
AQD7oePZ/QI6AuDMFmnJ5HQJ6cQSSOp/FwyucD8yiAoABrg4BGd0hYASCisGAQQB
|
||||
l1UBBQEBB0DBBk+BdzyTu3Hys6dewzd0L5AFeYMRIOC99XosMBLZYwMBCAeIeAQY
|
||||
FgoAIBYhBGV3iWZUbA6WJh3MR00wvB0X57PiBQJndIWAAhsMAAoJEE0wvB0X57Pi
|
||||
BxYA+gOtDVtb9p9vVaKyAuYCG7LdJ63Bqu0OtrVxmZEQvHQbAQDJmJX/yOX5QSkh
|
||||
cRtM1qoiY7OUjcT8ERnaoAcw2suFBLg4BGocy4ASCisGAQQBl1UBBQEBB0AgEDSA
|
||||
muuqS2iyi7aZeV608Xx2qdVqYKjREgiC3cu4YQMBCAeIfgQYFgoAJhYhBGV3iWZU
|
||||
bA6WJh3MR00wvB0X57PiBQJqHMuAAhsMBQkAJ40AAAoJEE0wvB0X57Pi9KgA/2vW
|
||||
4yNU1EF/WRCKmr87mJtmYR5Xf9Jps54Tv4BexNGgAQDxxx9lKvwW4U9/8UkrSF28
|
||||
HrGdWES4wAa9N9HvJzuOD7g4BGocy4ASCisGAQQBl1UBBQEBB0CK1TifDqKtFzks
|
||||
4E8gNmF90OkIYVriZxdY3QFSwowILQMBCAeIfgQYFgoAJhYhBGV3iWZUbA6WJh3M
|
||||
R00wvB0X57PiBQJqHMuAAhsMBQkAEnUAAAoJEE0wvB0X57PiZ+UA/0+s3fM/8OOb
|
||||
8Ft21QMFM/7Ce1P/ovKgkxsYWjk8q7zYAP9HYm/vcWBGzQw08mn3X26UFB9UfCF3
|
||||
qc1yyltDEeERDrg4BGomBgASCisGAQQBl1UBBQEBB0AvGvgG8Wkwx/KklkUHdLuC
|
||||
UKKSqTp4xsjocWXJs69rSwMBCAeIfgQYFgoAJhYhBGV3iWZUbA6WJh3MR00wvB0X
|
||||
57PiBQJqJgYAAhsMBQkAEnUAAAoJEE0wvB0X57PibzQA/A0qT23pcdenNJJ5QZN/
|
||||
ecs08p2pbiipv+adPeojVGZIAP47Pyr8Hj0o3qhrEhhFZkIhTxxDxS/jAsq2v4UG
|
||||
JGp8Ag==
|
||||
=RzZZ
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
";
|
||||
|
||||
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
|
||||
|
||||
// Two-week subkey that was generated earlier is selected
|
||||
// because it expires earlier, on 2026-06-15.
|
||||
{
|
||||
let ts = u32::try_from(
|
||||
Utc.with_ymd_and_hms(2026, 6, 9, 0, 0, 0)
|
||||
.unwrap()
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap();
|
||||
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
subkey.fingerprint().to_string().as_str(),
|
||||
"477650c13fa4842baf2831bc41c2d560be27b38d",
|
||||
);
|
||||
}
|
||||
|
||||
// Now both 477650C13FA4842BAF2831BC41C2D560BE27B38D expires
|
||||
// and we switch to another subkey that expires on 2026-06-22.
|
||||
{
|
||||
let ts = u32::try_from(
|
||||
Utc.with_ymd_and_hms(2026, 6, 16, 0, 0, 0)
|
||||
.unwrap()
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap();
|
||||
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
subkey.fingerprint().to_string().as_str(),
|
||||
"ab92f4afd46ddd7d31fb05151be08a8089f67ca2",
|
||||
);
|
||||
}
|
||||
|
||||
// Now both two-week subkeys have expired and we switch to expiring subkey that expires on 2026-07-01.
|
||||
{
|
||||
let ts = u32::try_from(
|
||||
Utc.with_ymd_and_hms(2026, 6, 23, 0, 0, 0)
|
||||
.unwrap()
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap();
|
||||
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
subkey.fingerprint().to_string().as_str(),
|
||||
"10493ef4dba7ee4d55c826a7634684d9bfeeb890",
|
||||
);
|
||||
}
|
||||
|
||||
// All expiring subkeys are expired, switching to non-expiring subkey.
|
||||
{
|
||||
let ts = u32::try_from(
|
||||
Utc.with_ymd_and_hms(2026, 7, 2, 0, 0, 0)
|
||||
.unwrap()
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap();
|
||||
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
|
||||
assert_eq!(
|
||||
subkey.fingerprint().to_string().as_str(),
|
||||
"ce5fc3fd479e41f08069dce6f6cad4adb9af47f1",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
//!
|
||||
//! Pinned messages are synchronized for all chat members by sending an info-message
|
||||
//! that refers the pinned message in the `In-Reply-To:` header.
|
||||
//! The info-message is only shown for pinning (not for unpinning).
|
||||
//! Unpinning is an action that does not require so much attention; internally it is a hidden info-message.
|
||||
//! Info-message are shown for both, pinning and unpinning;
|
||||
//! the latter is important as otherwise the message "Alice pinned a message." may look like a bug
|
||||
//! if there is nothing actually pinned as Bob unpinned for whatever reason afterwards.
|
||||
|
||||
use anyhow::{Result, ensure};
|
||||
|
||||
@@ -50,9 +51,8 @@ pub async fn set_pinned_state(
|
||||
info_msg.text = if new_pinned_state {
|
||||
stock_str::msg_pinned(context, ContactId::SELF).await
|
||||
} else {
|
||||
"Message unpinned.".to_string() // no need to localize, "unpinned" messages are not visible
|
||||
stock_str::msg_unpinned(context, ContactId::SELF).await
|
||||
};
|
||||
info_msg.hidden = !new_pinned_state;
|
||||
info_msg.in_reply_to = Some(msg.rfc724_mid.clone());
|
||||
info_msg.param.set_cmd(if new_pinned_state {
|
||||
SystemMessage::MessagePinned
|
||||
|
||||
@@ -76,21 +76,20 @@ async fn test_pinned_messages() -> Result<()> {
|
||||
assert!(set_pinned_state(alice, info_msg.id, true).await.is_err());
|
||||
|
||||
// Unpin the initially pinned message.
|
||||
// Before, send another message "Bar". To test, no visible info message is added this time,
|
||||
let sent3 = alice.send_text(alice_chat_id, "Bar").await;
|
||||
|
||||
set_pinned_state(alice, msg1.id, false).await?;
|
||||
let sent4 = alice.pop_sent_msg().await;
|
||||
let sent3 = alice.pop_sent_msg().await;
|
||||
assert!(!sent1.load_from_db().await.is_pinned());
|
||||
expect_pinned_message_event(alice, msg1.chat_id).await;
|
||||
|
||||
let pinned = get_pinned_messages(alice, alice_chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let msg3 = sent3.load_from_db().await;
|
||||
assert!(!msg3.is_info());
|
||||
assert!(!msg3.is_pinned());
|
||||
assert_eq!(alice.get_last_msg_id_in(msg3.chat_id).await, msg3.id); // last message is still "Bar", not an info message
|
||||
let unpin_msg = sent3.load_from_db().await;
|
||||
assert!(unpin_msg.is_info());
|
||||
assert!(!unpin_msg.hidden);
|
||||
assert_eq!(unpin_msg.get_info_type(), SystemMessage::MessageUnpinned);
|
||||
assert_eq!(unpin_msg.text, "You unpinned a message.");
|
||||
assert_eq!(alice.get_last_msg_id_in(msg1.chat_id).await, unpin_msg.id);
|
||||
|
||||
if alice_chat_id != self_chat_id {
|
||||
// Bob receives message "Foo"
|
||||
@@ -115,19 +114,19 @@ async fn test_pinned_messages() -> Result<()> {
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::MessagePinned);
|
||||
assert!(info_msg.get_info_contact_id(bob).await?.is_none());
|
||||
|
||||
// Bob receives message "Bar" and hidden message to unpin message "Foo"
|
||||
bob.recv_msg(&sent3).await;
|
||||
bob.recv_msg_trash(&sent4).await;
|
||||
// Bob receives the info message to unpin message "Foo"
|
||||
let unpin_msg = bob.recv_msg(&sent3).await;
|
||||
expect_pinned_message_event(bob, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(bob, msg1.id).await?.is_pinned());
|
||||
|
||||
let pinned = get_pinned_messages(bob, msg1.chat_id).await?;
|
||||
assert!(pinned.is_empty());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(bob, bob.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
assert!(unpin_msg.is_info());
|
||||
assert!(!unpin_msg.hidden);
|
||||
assert_eq!(unpin_msg.get_info_type(), SystemMessage::MessageUnpinned);
|
||||
assert_eq!(unpin_msg.state, MessageState::InNoticed); // no notification, no badge
|
||||
assert_eq!(bob.get_last_msg_id_in(msg1.chat_id).await, unpin_msg.id);
|
||||
}
|
||||
|
||||
// Alice's second device receives all four messages and ends up in the same state
|
||||
@@ -136,15 +135,11 @@ async fn test_pinned_messages() -> Result<()> {
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
alice2.recv_msg(&sent3).await;
|
||||
alice2.recv_msg_trash(&sent4).await;
|
||||
let unpin_msg = alice2.recv_msg(&sent3).await;
|
||||
expect_pinned_message_event(alice2, msg1.chat_id).await;
|
||||
assert!(!Message::load_from_db(alice2, msg1.id).await?.is_pinned());
|
||||
|
||||
let no_info_msg =
|
||||
Message::load_from_db(alice2, alice2.get_last_msg_id_in(msg1.chat_id).await).await?;
|
||||
assert!(!no_info_msg.is_info());
|
||||
assert_eq!(no_info_msg.text, "Bar");
|
||||
assert_eq!(unpin_msg.get_info_type(), SystemMessage::MessageUnpinned);
|
||||
assert_eq!(unpin_msg.text, "You unpinned a message.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -156,14 +156,6 @@ mod tests {
|
||||
};
|
||||
assert!(!t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
t.evtracker.clear_events();
|
||||
t.set_primary_self_addr("new@addr").await?;
|
||||
assert!(t.quota.read().await.is_empty());
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
|
||||
.await;
|
||||
assert!(t.quota_needs_update(0, TIMEOUT).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,9 +354,12 @@ pub(crate) fn refine_frequencies(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::create_broadcast;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::test_utils::{TestContextManager, sync};
|
||||
use crate::tools::SystemTime;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_reaction_wire_format() {
|
||||
@@ -583,6 +586,34 @@ mod tests {
|
||||
assert_eq!(result[1].is_from_self, true);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_reactions_not_in_chatlist_summary() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat_id, "hi channel!").await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
// reactions show up in the summary only if newer than the message; make sure that is the case.
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
|
||||
send_reaction(bob, bob_msg.id, "❤️").await?;
|
||||
alice.recv_msg_hidden(&bob.pop_sent_msg().await).await;
|
||||
|
||||
for t in [alice, bob] {
|
||||
let chatlist = Chatlist::try_load(t, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(t, 0, None).await?;
|
||||
assert_eq!(summary.text, "hi channel!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_channel_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -721,6 +752,67 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The channel owner has two devices.
|
||||
/// A reaction summary sent by one device must not make the other device
|
||||
/// broadcast the summary again, otherwise the devices send summaries to each other
|
||||
/// (and to all subscribers) every `REACTION_BROADCAST_PERIOD` forever.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_reactions_multidevice_no_loop() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
for a in [alice1, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
|
||||
let alice1_chat_id = create_broadcast(alice1, "Channel".to_string()).await?;
|
||||
sync(alice1, alice2).await;
|
||||
let qr = get_securejoin_qr(alice1, Some(alice1_chat_id)).await?;
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice1, &qr).await;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
tcm.section("Alice1 posts, Alice2 and Bob receive the post");
|
||||
let sent_msg = alice1.send_text(alice1_chat_id, "hi channel!").await;
|
||||
let alice1_msg_id = sent_msg.load_from_db().await.id;
|
||||
let alice2_msg = alice2.recv_msg(&sent_msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
tcm.section("Alice2 reacts to her own post, Alice1 learns about it");
|
||||
send_reaction(alice2, alice2_msg.id, "👍").await?;
|
||||
alice1.recv_msg_hidden(&alice2.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
get_msg_reactions(alice1, alice1_msg_id).await?.to_string(),
|
||||
"👍1"
|
||||
);
|
||||
broadcast_reactions_for_all_chats(alice2).await?; // bypass timer
|
||||
let summary = alice2.pop_sent_msg().await;
|
||||
alice1.recv_msg_hidden(&summary).await;
|
||||
bob.recv_msg_hidden(&summary).await;
|
||||
|
||||
tcm.section("Bob reacts, Alice1 broadcasts the summary");
|
||||
send_reaction(bob, bob_msg.id, "❤️").await?;
|
||||
alice1.recv_msg_hidden(&bob.pop_sent_msg().await).await;
|
||||
broadcast_reactions_for_all_chats(alice1).await?; // bypass timer
|
||||
let summary = alice1.pop_sent_msg().await;
|
||||
bob.recv_msg_hidden(&summary).await;
|
||||
assert_eq!(
|
||||
get_msg_reactions(bob, bob_msg.id).await?.to_string(),
|
||||
"❤️1 👍1"
|
||||
);
|
||||
|
||||
tcm.section("Alice2 receives Alice1's summary and must not broadcast it again");
|
||||
alice2.recv_msg_hidden(&summary).await;
|
||||
broadcast_reactions_for_all_chats(alice2).await?;
|
||||
assert!(alice2.pop_sent_msg_opt().await.is_none());
|
||||
|
||||
tcm.section("The summary must not remove Alice's own reaction on Alice2");
|
||||
let reactions = get_msg_reactions(alice2, alice2_msg.id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_subscriber_sends_unallowed_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
+38
-20
@@ -483,7 +483,12 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
let trash = || async {
|
||||
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
|
||||
let msg_ids = vec![
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| insert_tombstone(tx, rfc724_mid, false))
|
||||
.await?,
|
||||
];
|
||||
Ok(Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -669,7 +674,10 @@ pub(crate) async fn receive_imf_inner(
|
||||
match res {
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
|
||||
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
|
||||
let msg_id = context
|
||||
.sql
|
||||
.transaction(|tx| insert_tombstone(tx, rfc724_mid, needs_delete_job))
|
||||
.await?;
|
||||
received_msg = Some(ReceivedMsg {
|
||||
chat_id: ChatId::TRASH,
|
||||
state: MessageState::InSeen,
|
||||
@@ -933,9 +941,15 @@ pub(crate) async fn receive_imf_inner(
|
||||
warn!(context, "MDN: Not encrypted. Ignoring.");
|
||||
continue;
|
||||
}
|
||||
message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
|
||||
if let Err(e) = msg_id.start_ephemeral_timer(context).await {
|
||||
error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
|
||||
let trigger_ephemeral_interrupt = context
|
||||
.sql
|
||||
.transaction(|tx| {
|
||||
message::update_msg_state(tx, msg_id, MessageState::InSeen)?;
|
||||
msg_id.start_ephemeral_timer(tx)
|
||||
})
|
||||
.await?;
|
||||
if trigger_ephemeral_interrupt {
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
if !mime_parser.has_chat_version() {
|
||||
continue;
|
||||
@@ -1153,9 +1167,6 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message == SystemMessage::MessageUnpinned {
|
||||
info!(context, "Message unpinned (TRASH).");
|
||||
true
|
||||
} else if let Some(ref decryption_error) = mime_parser.decryption_error
|
||||
&& !mime_parser.incoming
|
||||
{
|
||||
@@ -1828,13 +1839,17 @@ async fn add_parts(
|
||||
EphemeralTimer::Disabled
|
||||
};
|
||||
|
||||
// silent messages are added as "noticed" and do not bubble up in the chatlist
|
||||
let silent =
|
||||
group_changes.silent || mime_parser.is_system_message == SystemMessage::MessageUnpinned;
|
||||
|
||||
let state = if !mime_parser.incoming {
|
||||
MessageState::OutDelivered
|
||||
} else if seen || !mime_parser.mdn_reports.is_empty() || chat_id_blocked == Blocked::Yes
|
||||
// No check for `hidden` because only reactions are such and they should be `InFresh`.
|
||||
{
|
||||
MessageState::InSeen
|
||||
} else if mime_parser.from.addr == STATISTICS_BOT_EMAIL || group_changes.silent {
|
||||
} else if mime_parser.from.addr == STATISTICS_BOT_EMAIL || silent {
|
||||
MessageState::InNoticed
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
@@ -1944,21 +1959,17 @@ async fn add_parts(
|
||||
|
||||
Some(better_msg)
|
||||
} else if mime_parser.is_system_message == SystemMessage::MessagePinned {
|
||||
Some(stock_str::msg_pinned(context, from_id).await) // message unpinned info is trashed in decide_chat_assignment()
|
||||
Some(stock_str::msg_pinned(context, from_id).await)
|
||||
} else if mime_parser.is_system_message == SystemMessage::MessageUnpinned {
|
||||
Some(stock_str::msg_unpinned(context, from_id).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
drop(chat); // Avoid using stale `chat` object.
|
||||
|
||||
let sort_timestamp = tweak_sort_timestamp(
|
||||
context,
|
||||
mime_parser,
|
||||
group_changes.silent,
|
||||
chat_id,
|
||||
sort_timestamp,
|
||||
)
|
||||
.await?;
|
||||
let sort_timestamp =
|
||||
tweak_sort_timestamp(context, mime_parser, silent, chat_id, sort_timestamp).await?;
|
||||
|
||||
let mime_in_reply_to = mime_parser
|
||||
.get_header(HeaderDef::InReplyTo)
|
||||
@@ -2106,7 +2117,11 @@ async fn add_parts(
|
||||
let mut parts = mime_parser.parts.iter().peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
let hidden = part.is_reaction;
|
||||
if part.is_reaction {
|
||||
// A reaction summary of a broadcast channel (`Chat-Broadcast-States:`) is sent as an empty
|
||||
// reaction to the latest channel message, but it is not a reaction of the sender.
|
||||
// Applying it would remove the owner's own reaction and, on another device of the owner,
|
||||
// queue the message for the next summary, so the devices would resend summaries forever.
|
||||
if part.is_reaction && mime_parser.broadcast_reactions.is_none() {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh =
|
||||
mime_parser.incoming && !seen && chat_id_blocked == Blocked::Not;
|
||||
@@ -2402,7 +2417,10 @@ async fn handle_edit_delete(
|
||||
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
|
||||
insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| insert_tombstone(tx, rfc724_mid, false))
|
||||
.await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
@@ -3853,7 +3853,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_chat_id = create_group(alice, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
@@ -4541,7 +4541,7 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let alice_addr = &alice.get_primary_self_addr().await?;
|
||||
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
|
||||
// participate in the scenario.
|
||||
@@ -5638,7 +5638,7 @@ async fn test_bcc_not_a_group() -> Result<()> {
|
||||
async fn test_lookup_key_contact_by_address_self() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let addr = &t.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let addr = &t.get_primary_self_addr().await?;
|
||||
assert_eq!(
|
||||
lookup_key_contact_by_address(t, addr, None).await?,
|
||||
Some(ContactId::SELF)
|
||||
@@ -5690,10 +5690,7 @@ async fn test_outgoing_determined_by_signature() -> Result<()> {
|
||||
let alice_dev2 = &tcm.unconfigured().await;
|
||||
alice_dev2.configure_addr(different_from).await;
|
||||
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
|
||||
assert_ne!(
|
||||
alice.get_config(Config::ConfiguredAddr).await?.unwrap(),
|
||||
different_from
|
||||
);
|
||||
assert_ne!(alice.get_primary_self_addr().await?, different_from);
|
||||
|
||||
// Send message from alice_dev2 and check alice sees it as outgoing
|
||||
let chat_id = alice_dev2.create_chat_id(bob).await;
|
||||
|
||||
+35
-47
@@ -4,9 +4,8 @@ use std::num::NonZeroUsize;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use futures::future::try_join_all;
|
||||
use futures_lite::FutureExt;
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio::task::{self, JoinSet};
|
||||
@@ -319,7 +318,7 @@ impl SchedulerState {
|
||||
}
|
||||
|
||||
while let Some(fetched) = set.join_next().await {
|
||||
if let Ok(true) = fetched {
|
||||
if fetched.context("background_fetch_any: Failed to join a task")? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -403,11 +402,7 @@ pub(crate) struct Scheduler {
|
||||
recently_seen_loop: RecentlySeenLoop,
|
||||
}
|
||||
|
||||
async fn inbox_loop(
|
||||
ctx: Context,
|
||||
started: oneshot::Sender<()>,
|
||||
inbox_handlers: ImapConnectionHandlers,
|
||||
) {
|
||||
async fn inbox_loop(ctx: Context, inbox_handlers: ImapConnectionHandlers) {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
info!(ctx, "Starting inbox loop.");
|
||||
@@ -420,10 +415,6 @@ async fn inbox_loop(
|
||||
let ctx1 = ctx.clone();
|
||||
let fut = async move {
|
||||
let ctx = ctx1;
|
||||
if let Err(()) = started.send(()) {
|
||||
warn!(ctx, "Inbox loop, missing started receiver.");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut old_session: Option<Session> = None;
|
||||
loop {
|
||||
@@ -611,11 +602,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
async fn smtp_loop(
|
||||
ctx: Context,
|
||||
started: oneshot::Sender<()>,
|
||||
smtp_handlers: SmtpConnectionHandlers,
|
||||
) {
|
||||
async fn smtp_loop(ctx: Context, smtp_handlers: SmtpConnectionHandlers) {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
info!(ctx, "Starting SMTP loop.");
|
||||
@@ -628,10 +615,6 @@ async fn smtp_loop(
|
||||
let ctx1 = ctx.clone();
|
||||
let fut = async move {
|
||||
let ctx = ctx1;
|
||||
if let Err(()) = started.send(()) {
|
||||
warn!(&ctx, "SMTP loop, missing started receiver.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reschedule the check to catch changes lost to a restart.
|
||||
schedule_keyupdate_check(&ctx).await.log_err(&ctx).ok();
|
||||
@@ -678,11 +661,11 @@ async fn smtp_loop(
|
||||
"SMTP has messages to retry, planning to retry {t} seconds later."
|
||||
);
|
||||
let duration = std::time::Duration::from_secs(t);
|
||||
tokio::time::timeout(duration, async {
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if let Ok(Err(async_channel::RecvError)) =
|
||||
tokio::time::timeout(duration, idle_interrupt_receiver.recv()).await
|
||||
{
|
||||
break;
|
||||
}
|
||||
let slept = time_elapsed(&now).as_secs();
|
||||
timeout = Some(cmp::max(
|
||||
t,
|
||||
@@ -706,11 +689,15 @@ async fn smtp_loop(
|
||||
info!(ctx, "SMTP has no messages to retry, waiting for interrupt.");
|
||||
let interrupt = idle_interrupt_receiver.recv();
|
||||
if next_check != 0 {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(wait), interrupt)
|
||||
.await
|
||||
.ok();
|
||||
if let Ok(Err(async_channel::RecvError)) =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(wait), interrupt).await
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
interrupt.await.ok();
|
||||
if let Err(async_channel::RecvError) = interrupt.await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -732,20 +719,17 @@ impl Scheduler {
|
||||
pub async fn start(ctx: &Context) -> Result<Self> {
|
||||
let (smtp, smtp_handlers) = SmtpConnectionState::new();
|
||||
|
||||
let (smtp_start_send, smtp_start_recv) = oneshot::channel();
|
||||
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
|
||||
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
|
||||
|
||||
let mut inboxes = Vec::new();
|
||||
let mut start_recvs = Vec::new();
|
||||
|
||||
for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? {
|
||||
let (conn_state, inbox_handlers) =
|
||||
ImapConnectionState::new(ctx, transport_id, configured_login_param.clone()).await?;
|
||||
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
|
||||
let handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
|
||||
task::spawn(inbox_loop(ctx, inbox_handlers))
|
||||
};
|
||||
let addr = configured_login_param.addr.clone();
|
||||
let folder = configured_login_param
|
||||
@@ -758,14 +742,12 @@ impl Scheduler {
|
||||
handle,
|
||||
};
|
||||
inboxes.push(inbox);
|
||||
start_recvs.push(inbox_start_recv);
|
||||
}
|
||||
|
||||
let smtp_handle = {
|
||||
let ctx = ctx.clone();
|
||||
task::spawn(smtp_loop(ctx, smtp_start_send, smtp_handlers))
|
||||
task::spawn(smtp_loop(ctx, smtp_handlers))
|
||||
};
|
||||
start_recvs.push(smtp_start_recv);
|
||||
|
||||
let ephemeral_handle = {
|
||||
let ctx = ctx.clone();
|
||||
@@ -794,11 +776,6 @@ impl Scheduler {
|
||||
recently_seen_loop,
|
||||
};
|
||||
|
||||
// wait for all loops to be started
|
||||
if let Err(err) = try_join_all(start_recvs).await {
|
||||
bail!("failed to start scheduler: {err}");
|
||||
}
|
||||
|
||||
info!(ctx, "scheduler is running");
|
||||
Ok(res)
|
||||
}
|
||||
@@ -851,20 +828,31 @@ impl Scheduler {
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
|
||||
let tracker = TaskTracker::new();
|
||||
for b in self.inboxes {
|
||||
for mut b in self.inboxes {
|
||||
let context = context.clone();
|
||||
tracker.spawn(async move {
|
||||
tokio::time::timeout(timeout_duration, b.handle)
|
||||
if tokio::time::timeout(timeout_duration, &mut b.handle)
|
||||
.await
|
||||
.log_err(&context)
|
||||
.is_err()
|
||||
{
|
||||
warn!(context, "IMAP shutdown timed out; aborting task.");
|
||||
b.handle.abort();
|
||||
b.handle.await.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let context = context.clone();
|
||||
tracker.spawn(async move {
|
||||
tokio::time::timeout(timeout_duration, self.smtp_handle)
|
||||
let mut handle = self.smtp_handle;
|
||||
if tokio::time::timeout(timeout_duration, &mut handle)
|
||||
.await
|
||||
.log_err(&context)
|
||||
.is_err()
|
||||
{
|
||||
warn!(context, "SMTP shutdown timed out; aborting task.");
|
||||
handle.abort();
|
||||
handle.await.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
tracker.close();
|
||||
|
||||
+5
-2
@@ -33,7 +33,7 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_').remove(b'@');
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
|
||||
fn inviter_progress(
|
||||
context: &Context,
|
||||
@@ -543,7 +543,10 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
)
|
||||
.await?;
|
||||
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_message).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| insert_into_smtp(tx, &rfc724_mid, &queued_message))
|
||||
.await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
|
||||
Ok(HandshakeMessage::Done)
|
||||
|
||||
@@ -339,7 +339,10 @@ pub(crate) async fn send_handshake_message(
|
||||
recipients.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_msg).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| insert_into_smtp(tx, &rfc724_mid, &queued_msg))
|
||||
.await?;
|
||||
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
} else {
|
||||
|
||||
@@ -50,11 +50,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_addr = alice.get_primary_self_addr().await.unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
@@ -118,7 +114,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
assert_eq!(sent.recipients, *alice_addr);
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.signature.is_none());
|
||||
assert_eq!(
|
||||
@@ -1160,12 +1156,12 @@ async fn test_get_securejoin_qr_encoding() -> Result<()> {
|
||||
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
assert!(
|
||||
qr.contains("a=jk%25l@example.net"),
|
||||
"{qr} doesn't contain 'a=jk%25l@example.net'"
|
||||
qr.contains("a=jk%25l%40example.net"),
|
||||
"{qr} doesn't contain 'a=jk%25l%40example.net'"
|
||||
);
|
||||
assert!(
|
||||
qr.contains("r=asdf@example.org,alice@example.org"),
|
||||
"{qr} doesn't contain 'r=asdf@example.org,alice@example.org'"
|
||||
qr.contains("r=asdf%40example.org,alice%40example.org"),
|
||||
"{qr} doesn't contain 'r=asdf%40example.org,alice%40example.org'"
|
||||
);
|
||||
|
||||
let qr = check_qr(bob, &qr).await?;
|
||||
|
||||
+133
-97
@@ -13,7 +13,6 @@ use rusqlite::OptionalExtension as _;
|
||||
use tokio::task;
|
||||
|
||||
use crate::chat::{ChatId, add_info_msg_with_cmd};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -32,21 +31,26 @@ use crate::transport::{
|
||||
ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
|
||||
};
|
||||
|
||||
/// SMTP connection with information about it.
|
||||
#[derive(Debug)]
|
||||
struct Connection {
|
||||
transport: SmtpTransport<Box<dyn SessionBufStream>>,
|
||||
|
||||
/// Email address that should be put into the From field when sending over this connection.
|
||||
from: EmailAddress,
|
||||
|
||||
/// ID of the transport from which SMTP connection configuration comes from.
|
||||
transport_id: u32,
|
||||
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded).
|
||||
last_success: tools::Time,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Smtp {
|
||||
/// SMTP connection.
|
||||
transport: Option<SmtpTransport<Box<dyn SessionBufStream>>>,
|
||||
|
||||
/// Email address we are sending from.
|
||||
from: Option<EmailAddress>,
|
||||
|
||||
/// Transport we are connected to.
|
||||
transport_id: Option<u32>,
|
||||
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
last_success: Option<tools::Time>,
|
||||
connection: Option<Connection>,
|
||||
|
||||
pub(crate) connectivity: ConnectivityStore,
|
||||
|
||||
@@ -61,7 +65,10 @@ async fn sorted_transports(context: &Context) -> Result<Vec<(u32, ConfiguredLogi
|
||||
.query_map_vec(
|
||||
"SELECT transports.id, configured_param FROM transports
|
||||
LEFT JOIN smtp_success ON smtp_success.transport_id=transports.id
|
||||
ORDER BY IFNULL(smtp_success.id, 0) DESC, transports.id ASC",
|
||||
LEFT JOIN smtp_failure ON smtp_failure.transport_id=transports.id
|
||||
ORDER BY IFNULL(smtp_success.id, 0) DESC,
|
||||
IFNULL(smtp_failure.id, 0) ASC,
|
||||
transports.id ASC",
|
||||
(),
|
||||
|row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
@@ -79,10 +86,37 @@ async fn record_success(context: &Context, transport_id: u32) -> Result<()> {
|
||||
// if the row exists already, so it becomes the highest rowid in the table.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO smtp_success (transport_id) VALUES (?)",
|
||||
(transport_id,),
|
||||
)
|
||||
.transaction(|t| {
|
||||
t.execute(
|
||||
"INSERT OR REPLACE INTO smtp_success (transport_id) VALUES (?)",
|
||||
(transport_id,),
|
||||
)?;
|
||||
t.execute(
|
||||
"DELETE FROM smtp_failure WHERE transport_id = ?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records a failure and removes last remembered success for
|
||||
/// SMTP transport so another transport is tried next time.
|
||||
async fn record_failure(context: &Context, transport_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
t.execute(
|
||||
"INSERT OR REPLACE INTO smtp_failure (transport_id) VALUES (?)",
|
||||
(transport_id,),
|
||||
)?;
|
||||
t.execute(
|
||||
"DELETE FROM smtp_success WHERE transport_id = ?",
|
||||
(transport_id,),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -95,30 +129,25 @@ impl Smtp {
|
||||
|
||||
/// Disconnect the SMTP transport and drop it entirely.
|
||||
pub fn disconnect(&mut self) {
|
||||
if let Some(mut transport) = self.transport.take() {
|
||||
if let Some(mut connection) = self.connection.take() {
|
||||
// Closing connection with a QUIT command may take some time, especially if it's a
|
||||
// stale connection and an attempt to send the command times out. Send a command in a
|
||||
// separate task to avoid waiting for reply or timeout.
|
||||
task::spawn(async move { transport.quit().await });
|
||||
task::spawn(async move { connection.transport.quit().await });
|
||||
}
|
||||
self.transport_id = None;
|
||||
self.from = None;
|
||||
self.last_success = None;
|
||||
}
|
||||
|
||||
/// Return true if smtp was connected but is not known to
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
time_elapsed(&last_success).as_secs() > 60
|
||||
} else {
|
||||
false
|
||||
}
|
||||
self.connection
|
||||
.as_ref()
|
||||
.is_some_and(|connection| time_elapsed(&connection.last_success).as_secs() > 60)
|
||||
}
|
||||
|
||||
/// Check whether we are connected.
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.transport.is_some()
|
||||
self.connection.is_some()
|
||||
}
|
||||
|
||||
/// Connect using configured parameters.
|
||||
@@ -143,14 +172,12 @@ impl Smtp {
|
||||
&lp.smtp_password,
|
||||
&proxy_config,
|
||||
&lp.addr,
|
||||
transport_id,
|
||||
lp.strict_tls(proxy_config.is_some())?,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
self.transport_id = Some(transport_id);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
@@ -163,6 +190,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -170,6 +198,7 @@ impl Smtp {
|
||||
password: &str,
|
||||
proxy_config: &Option<ProxyConfig>,
|
||||
addr: &str,
|
||||
transport_id: u32,
|
||||
strict_tls: bool,
|
||||
) -> Result<()> {
|
||||
if self.is_connected() {
|
||||
@@ -179,7 +208,6 @@ impl Smtp {
|
||||
|
||||
let from = EmailAddress::new(addr.to_string())
|
||||
.with_context(|| format!("Invalid address {addr:?}"))?;
|
||||
self.from = Some(from);
|
||||
|
||||
let login_params =
|
||||
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
|
||||
@@ -204,8 +232,12 @@ impl Smtp {
|
||||
}
|
||||
};
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.last_success = Some(tools::Time::now());
|
||||
self.connection = Some(Connection {
|
||||
transport,
|
||||
from,
|
||||
transport_id,
|
||||
last_success: tools::Time::now(),
|
||||
});
|
||||
|
||||
context.emit_event(EventType::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
@@ -230,7 +262,7 @@ pub(crate) enum SendResult {
|
||||
}
|
||||
|
||||
/// Tries to send a message.
|
||||
pub(crate) async fn smtp_send(
|
||||
async fn smtp_send(
|
||||
context: &Context,
|
||||
recipients: &[async_smtp::EmailAddress],
|
||||
message: &str,
|
||||
@@ -246,14 +278,37 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
smtp.connectivity.set_working(context);
|
||||
|
||||
let send_result = smtp.send(context, recipients, message.as_bytes()).await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
|
||||
let Some(ref mut connection) = smtp.connection else {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to send a message because SMTP client is not connected."
|
||||
);
|
||||
return SendResult::Retry;
|
||||
};
|
||||
let transport_id = connection.transport_id;
|
||||
|
||||
let send_result = connection
|
||||
.send(context, recipients, message.as_bytes())
|
||||
.await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| format!("{e:#}"));
|
||||
|
||||
let status = match send_result {
|
||||
Err(crate::smtp::send::Error::SmtpSend(err)) => {
|
||||
Err(send::Error::SmtpSend(err)) => {
|
||||
// Remote error, retry later.
|
||||
info!(context, "SMTP failed to send: {:?}.", &err);
|
||||
|
||||
// Record failure on any error returned by the server, but not network errors.
|
||||
if matches!(
|
||||
err,
|
||||
async_smtp::error::Error::Permanent(_) | async_smtp::error::Error::Transient(_)
|
||||
) && let Err(err) = record_failure(context, transport_id).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Failed to remove success for {transport_id}: {err:#}."
|
||||
)
|
||||
}
|
||||
|
||||
let res = match err {
|
||||
async_smtp::error::Error::Permanent(ref response) => {
|
||||
// Workaround for incorrectly configured servers returning permanent errors
|
||||
@@ -327,44 +382,28 @@ pub(crate) async fn smtp_send(
|
||||
}
|
||||
};
|
||||
|
||||
// this clears last_success info
|
||||
info!(context, "Failed to send message over SMTP, disconnecting.");
|
||||
smtp.disconnect();
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "SMTP job is invalid: {err:#}.");
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
Err(crate::smtp::send::Error::NoTransport) => {
|
||||
// Should never happen.
|
||||
// It does not even make sense to disconnect here.
|
||||
error!(context, "SMTP job failed because SMTP has no transport.");
|
||||
SendResult::Failure(format_err!("SMTP has not transport"))
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
Err(send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "Unable to load SMTP job: {err:#}.");
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
Ok(()) => SendResult::Success,
|
||||
};
|
||||
|
||||
if matches!(status, SendResult::Success) {
|
||||
debug_assert!(smtp.transport_id.is_some());
|
||||
if let Some(transport_id) = smtp.transport_id
|
||||
&& let Err(err) = record_success(context, transport_id).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Failed to record successful use of transport {transport_id} in smtp_success table: {err:#}."
|
||||
);
|
||||
Ok(()) => {
|
||||
record_success(context, transport_id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to record successful use of transport {transport_id}")
|
||||
})
|
||||
.log_err(context)
|
||||
.ok();
|
||||
SendResult::Success
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let SendResult::Failure(err) = &status
|
||||
&& let Some(msg_id) = msg_id
|
||||
@@ -390,28 +429,21 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
/// Inserts a tombstone for `rfc724_mid`
|
||||
/// and queues the message for SMTP sending.
|
||||
pub(crate) async fn insert_into_smtp(
|
||||
context: &Context,
|
||||
pub(crate) fn insert_into_smtp(
|
||||
tx: &mut rusqlite::Transaction<'_>,
|
||||
rfc724_mid: &str,
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
.await?;
|
||||
let msg_id = message::insert_tombstone(tx, rfc724_mid, false)?;
|
||||
queue::enqueue_mail(tx, now, msg_id, queued_msg, None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends message identified by `smtp` table rowid over SMTP connection.
|
||||
///
|
||||
/// Removes row if the message should not be retried, otherwise increments retry count.
|
||||
pub(crate) async fn send_msg_to_smtp(
|
||||
context: &Context,
|
||||
smtp: &mut Smtp,
|
||||
rowid: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
pub(crate) async fn send_msg_to_smtp(context: &Context, smtp: &mut Smtp, rowid: i64) -> Result<()> {
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
@@ -472,9 +504,10 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
let mut recipients = queued_mail.recipients.clone();
|
||||
if queued_mail.bcc_self {
|
||||
let from_addr = smtp
|
||||
.from
|
||||
.connection
|
||||
.as_ref()
|
||||
.context("No From address available, likely not connected")?
|
||||
.context("Not connected")?
|
||||
.from
|
||||
.to_string();
|
||||
add_self_recipients(
|
||||
context,
|
||||
@@ -504,19 +537,24 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from_addr = smtp
|
||||
.from
|
||||
.connection
|
||||
.as_ref()
|
||||
.context("No From address available, likely not connected")?
|
||||
.context("Not connected")?
|
||||
.from
|
||||
.to_string();
|
||||
|
||||
let transport_id = smtp.transport_id.context("Not connected")?;
|
||||
let transport_id = smtp
|
||||
.connection
|
||||
.as_ref()
|
||||
.context("Not connected")?
|
||||
.transport_id;
|
||||
let chunk_size = context
|
||||
.get_max_smtp_rcpt_to(transport_id, &from_addr)
|
||||
.await?
|
||||
.max(1);
|
||||
|
||||
let rendered_mail =
|
||||
mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?;
|
||||
mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr.clone())?;
|
||||
let body = rendered_mail.message;
|
||||
|
||||
info!(
|
||||
@@ -587,15 +625,9 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.await?;
|
||||
|
||||
if let Some((chat_id, timestamp_sort)) = res {
|
||||
let addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let text = unencrypted_email(
|
||||
context,
|
||||
addr.unwrap_or_default()
|
||||
.split('@')
|
||||
.nth(1)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
let text =
|
||||
unencrypted_email(context, from_addr.split('@').nth(1).unwrap_or_default())
|
||||
.await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
@@ -760,9 +792,10 @@ async fn send_mdn_rfc724_mid(
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from = smtp
|
||||
.from
|
||||
.connection
|
||||
.as_ref()
|
||||
.context("No From address, not connected")?
|
||||
.context("Not connected")?
|
||||
.from
|
||||
.to_string();
|
||||
let rendered_msg =
|
||||
mimefactory::render_queued_mail(queued_mdn, &public_key, &secret_key, from.clone())?;
|
||||
@@ -780,7 +813,10 @@ async fn send_mdn_rfc724_mid(
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| message::insert_tombstone(tx, &rendered_msg.rfc724_mid, false))
|
||||
.await?;
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
|
||||
+9
-21
@@ -1,31 +1,27 @@
|
||||
//! # SMTP message sending
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_smtp::{EmailAddress, Envelope, SendableEmail};
|
||||
|
||||
use super::Smtp;
|
||||
use super::Connection;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::tools;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Envelope error: {}", _0)]
|
||||
Envelope(anyhow::Error),
|
||||
#[error("Send error: {}", _0)]
|
||||
SmtpSend(async_smtp::error::Error),
|
||||
#[error("SMTP has no transport")]
|
||||
NoTransport,
|
||||
#[error("{}", _0)]
|
||||
#[error("{:#}", _0)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
impl Connection {
|
||||
/// Send a prepared mail to recipients.
|
||||
/// On successful send out Ok() is returned.
|
||||
pub async fn send(
|
||||
pub(super) async fn send(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
recipients: &[EmailAddress],
|
||||
@@ -38,21 +34,13 @@ impl Smtp {
|
||||
context.ratelimit.write().await.send();
|
||||
}
|
||||
|
||||
let envelope =
|
||||
Envelope::new(self.from.clone(), recipients.to_vec()).map_err(Error::Envelope)?;
|
||||
let envelope = Envelope::new(Some(self.from.clone()), recipients.to_vec())
|
||||
.context("Envelope error")?;
|
||||
let mail = SendableEmail::new(envelope, message);
|
||||
|
||||
let Some(ref mut transport) = self.transport else {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to send a message because SMTP client has no SmtpTransport."
|
||||
);
|
||||
return Err(Error::NoTransport);
|
||||
};
|
||||
self.transport.send(mail).await.map_err(Error::SmtpSend)?;
|
||||
|
||||
transport.send(mail).await.map_err(Error::SmtpSend)?;
|
||||
|
||||
self.last_success = Some(tools::Time::now());
|
||||
self.last_success = tools::Time::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+50
-29
@@ -1,8 +1,25 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::transport;
|
||||
|
||||
async fn sorted_transport_addrs(t: &TestContext) -> Result<String> {
|
||||
let transports = super::sorted_transports(t).await?;
|
||||
let addrs: Vec<String> = transports
|
||||
.into_iter()
|
||||
.map(|(_transport_id, transport)| transport.addr)
|
||||
.collect();
|
||||
Ok(addrs.join(" "))
|
||||
}
|
||||
|
||||
async fn sorted_transport_ids(t: &TestContext) -> Result<Vec<u32>> {
|
||||
let transports = super::sorted_transports(t).await?;
|
||||
Ok(transports
|
||||
.into_iter()
|
||||
.map(|(transport_id, _transport)| transport_id)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_smtp_candidates() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -12,38 +29,42 @@ async fn test_smtp_candidates() -> Result<()> {
|
||||
transport::add_pseudo_transport(t, "bar@example.net").await?;
|
||||
transport::add_pseudo_transport(t, "baz@example.net").await?;
|
||||
|
||||
let transports = super::sorted_transports(t).await?;
|
||||
let [
|
||||
(transport_id1, ref transport1),
|
||||
(transport_id2, ref transport2),
|
||||
(transport_id3, ref transport3),
|
||||
] = transports[..]
|
||||
else {
|
||||
panic!("Unexpected number of transports");
|
||||
};
|
||||
|
||||
// By default first added transport is used first.
|
||||
assert_eq!(transport1.addr, "foo@example.net");
|
||||
assert_eq!(transport2.addr, "bar@example.net");
|
||||
assert_eq!(transport3.addr, "baz@example.net");
|
||||
assert_eq!(
|
||||
sorted_transport_addrs(t).await?,
|
||||
"foo@example.net bar@example.net baz@example.net"
|
||||
);
|
||||
assert_eq!(sorted_transport_ids(t).await?, [1, 2, 3]);
|
||||
|
||||
super::record_success(t, transport_id3).await?;
|
||||
let transports2 = super::sorted_transports(t).await?;
|
||||
assert_eq!(transports2[0].0, transport_id3);
|
||||
assert_eq!(transports2[1].0, transport_id1);
|
||||
assert_eq!(transports2[2].0, transport_id2);
|
||||
super::record_success(t, 3).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [3, 1, 2]);
|
||||
|
||||
super::record_success(t, transport_id2).await?;
|
||||
let transports3 = super::sorted_transports(t).await?;
|
||||
assert_eq!(transports3[0].0, transport_id2);
|
||||
assert_eq!(transports3[1].0, transport_id3);
|
||||
assert_eq!(transports3[2].0, transport_id1);
|
||||
super::record_success(t, 2).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [2, 3, 1]);
|
||||
|
||||
super::record_success(t, transport_id3).await?;
|
||||
let transports4 = super::sorted_transports(t).await?;
|
||||
assert_eq!(transports4[0].0, transport_id3);
|
||||
assert_eq!(transports4[1].0, transport_id2);
|
||||
assert_eq!(transports4[2].0, transport_id1);
|
||||
super::record_success(t, 3).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [3, 2, 1]);
|
||||
|
||||
super::record_failure(t, 3).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [2, 1, 3]);
|
||||
|
||||
// New transport is added before all failing transports.
|
||||
transport::add_pseudo_transport(t, "qux@example.net").await?;
|
||||
assert_eq!(
|
||||
sorted_transport_addrs(t).await?,
|
||||
"bar@example.net foo@example.net qux@example.net baz@example.net"
|
||||
);
|
||||
assert_eq!(sorted_transport_ids(t).await?, [2, 1, 4, 3]);
|
||||
|
||||
super::record_success(t, 4).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [4, 2, 1, 3]);
|
||||
|
||||
super::record_failure(t, 4).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [2, 1, 3, 4]);
|
||||
super::record_failure(t, 3).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [2, 1, 4, 3]);
|
||||
super::record_failure(t, 2).await?;
|
||||
assert_eq!(sorted_transport_ids(t).await?, [1, 4, 3, 2]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2687,6 +2687,32 @@ CREATE TABLE smtp_success (
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 169)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"
|
||||
CREATE TABLE smtp_failure (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
transport_id INTEGER UNIQUE NOT NULL,
|
||||
FOREIGN KEY(transport_id) REFERENCES transports(id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 170)?;
|
||||
if dbversion < migration_version {
|
||||
// Lets `delete_messages()` find rows marked for deletion without reading the folder's rows.
|
||||
sql.execute_migration(
|
||||
"CREATE INDEX IF NOT EXISTS imap_pending_deletions
|
||||
ON imap(transport_id, folder) WHERE target != folder",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
+17
-1
@@ -431,6 +431,12 @@ https://delta.chat/donate"))]
|
||||
|
||||
#[strum(props(fallback = "Message pinned by %1$s."))]
|
||||
MsgMessagePinnedBy = 244,
|
||||
|
||||
#[strum(props(fallback = "You unpinned a message."))]
|
||||
MsgYouUnpinnedAMessage = 246,
|
||||
|
||||
#[strum(props(fallback = "Message unpinned by %1$s."))]
|
||||
MsgMessageUnpinnedBy = 247,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -617,7 +623,6 @@ pub(crate) async fn msg_chat_description_changed(
|
||||
}
|
||||
|
||||
/// Stock strings for pinning a message; used in info messages, once tapped, UI scrolled to the pinned message.
|
||||
/// For unpinning a message, we do not add a visible info message as this is of fewer interest.
|
||||
pub(crate) async fn msg_pinned(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouPinnedAMessage)
|
||||
@@ -627,6 +632,17 @@ pub(crate) async fn msg_pinned(context: &Context, by_contact: ContactId) -> Stri
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock strings for unpinning a message; used in info messages, once tapped, UI scrolled to the unpinned message.
|
||||
/// Unpinning messages are not notified, but still shown in the chat as otherwise the last message may read "USER pinned a message", but nothing is pinned.
|
||||
pub(crate) async fn msg_unpinned(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouUnpinnedAMessage)
|
||||
} else {
|
||||
translated(context, StockMessage::MsgMessageUnpinnedBy)
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string, one of:
|
||||
/// - `Member %1$s added.`,
|
||||
/// - `You added member %1$s.`,
|
||||
|
||||
+11
-9
@@ -7,7 +7,7 @@ use std::str;
|
||||
use anyhow::Result;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::calls::{CallState, call_state};
|
||||
use crate::calls::CallState;
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::DC_DESIRED_TEXT_LEN;
|
||||
@@ -68,9 +68,11 @@ impl Summary {
|
||||
chat: &Chat,
|
||||
contact: Option<&Contact>,
|
||||
) -> Result<Summary> {
|
||||
if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
|
||||
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
|
||||
.await?
|
||||
let is_broadcast = matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast);
|
||||
if !is_broadcast
|
||||
&& let Some((reaction_msg, reaction_contact_id, reaction)) = chat
|
||||
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
|
||||
.await?
|
||||
{
|
||||
// there is a reaction newer than the latest message, show that.
|
||||
// sorting and therefore date is still the one of the last message,
|
||||
@@ -237,11 +239,11 @@ impl Message {
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Call => {
|
||||
let call_info = context.load_call_by_id(self.id).await.unwrap_or(None);
|
||||
let has_video = call_info.is_some_and(|c| c.has_video_initially());
|
||||
let call_state = call_state(context, self.id)
|
||||
.await
|
||||
.unwrap_or(CallState::Alerting);
|
||||
let call_info = context.load_call_by_message(self.clone());
|
||||
let has_video = call_info.as_ref().is_some_and(|c| c.has_video_initially());
|
||||
let call_state = call_info
|
||||
.as_ref()
|
||||
.map_or(CallState::Alerting, |c| c.call_state());
|
||||
emoji = Some(if has_video { "🎥" } else { "📞" });
|
||||
type_name = Some(match call_state {
|
||||
CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
|
||||
|
||||
+50
-31
@@ -193,21 +193,11 @@ impl TestContextManager {
|
||||
to.recv_msg(&sent).await
|
||||
}
|
||||
|
||||
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
|
||||
self.section(&format!(
|
||||
"{} changes her self address and reconfigures",
|
||||
test_context.name()
|
||||
));
|
||||
|
||||
pub async fn change_addr(&self, test_context: &mut TestContext, new_addr: &str) {
|
||||
test_context.add_transport(new_addr).await;
|
||||
test_context.set_primary_self_addr(new_addr).await.unwrap();
|
||||
test_context.set_sending_addr(new_addr).await;
|
||||
// ensure_secret_key_exists() is called during configure
|
||||
key::ensure_secret_key_exists(test_context).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
test_context.get_primary_self_addr().await.unwrap(),
|
||||
new_addr
|
||||
);
|
||||
}
|
||||
|
||||
/// Executes SecureJoin protocol between `scanner` and `scanned`.
|
||||
@@ -252,10 +242,9 @@ impl TestContextManager {
|
||||
qr: &str,
|
||||
) -> ChatId {
|
||||
assert!(joiner.pop_sent_msg_opt().await.is_none());
|
||||
let inviter_addr = inviters[0].get_primary_self_addr().await.unwrap();
|
||||
let inviter_addr = inviters[0].sending_addr().await;
|
||||
for inviter in inviters {
|
||||
assert!(inviter.pop_sent_msg_opt().await.is_none());
|
||||
assert_eq!(inviter.get_primary_self_addr().await.unwrap(), inviter_addr);
|
||||
}
|
||||
|
||||
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
|
||||
@@ -435,6 +424,9 @@ pub struct TestContext {
|
||||
pub evtracker: EventTracker,
|
||||
|
||||
log_sink: LogSink,
|
||||
|
||||
/// From address to be used when emulating sending.
|
||||
from_for_test_msgs: Option<String>,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
@@ -530,6 +522,7 @@ impl TestContext {
|
||||
dir,
|
||||
evtracker: EventTracker::new(evtracker_receiver),
|
||||
log_sink,
|
||||
from_for_test_msgs: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,10 +553,6 @@ impl TestContext {
|
||||
add_pseudo_transport(&self.ctx, addr)
|
||||
.await
|
||||
.expect("Failed to add pseudo transport");
|
||||
self.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.expect("Failed to configure address");
|
||||
|
||||
if let Some(name) = addr.split('@').next() {
|
||||
self.set_name(name);
|
||||
@@ -586,6 +575,28 @@ impl TestContext {
|
||||
self.self_public_key.lock().await.take();
|
||||
}
|
||||
|
||||
pub async fn set_sending_addr(&mut self, addr: &str) {
|
||||
if !self
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM transports WHERE addr=?", (addr,))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
panic!("Address {addr} is not configured");
|
||||
}
|
||||
self.from_for_test_msgs = Some(addr.to_string());
|
||||
}
|
||||
|
||||
pub async fn sending_addr(&self) -> String {
|
||||
if let Some(ref from) = self.from_for_test_msgs {
|
||||
from.clone()
|
||||
} else {
|
||||
self.get_primary_self_addr()
|
||||
.await
|
||||
.expect("Cannot get From address")
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a sent message from the jobs table.
|
||||
///
|
||||
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
||||
@@ -632,10 +643,7 @@ ORDER BY id"
|
||||
.await
|
||||
.expect("Failed to load queued mail");
|
||||
if queued_mail.bcc_self {
|
||||
let from = self
|
||||
.get_primary_self_addr()
|
||||
.await
|
||||
.expect("Cannot get From address");
|
||||
let from = self.sending_addr().await;
|
||||
smtp::add_self_recipients(
|
||||
&self.ctx,
|
||||
&mut queued_mail.recipients,
|
||||
@@ -734,10 +742,7 @@ ORDER BY id"
|
||||
.await
|
||||
.expect("Failed to load queued mail");
|
||||
if queued_mail.bcc_self {
|
||||
let from = self
|
||||
.get_primary_self_addr()
|
||||
.await
|
||||
.expect("Cannot get self address");
|
||||
let from = self.sending_addr().await;
|
||||
smtp::add_self_recipients(
|
||||
&self.ctx,
|
||||
&mut queued_mail.recipients,
|
||||
@@ -895,9 +900,23 @@ ORDER BY id"
|
||||
.expect("failed to load msg")
|
||||
}
|
||||
|
||||
/// Returns the `EXPLAIN QUERY PLAN` steps of `query`.
|
||||
pub async fn query_plan(&self, query: &str) -> Result<Vec<String>> {
|
||||
self.sql
|
||||
.call(true, |conn| {
|
||||
let mut stmt = conn.prepare(&format!("EXPLAIN QUERY PLAN {query}"))?;
|
||||
let plan = stmt
|
||||
.raw_query()
|
||||
.mapped(|row| row.get::<_, String>(3))
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(plan)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
|
||||
pub async fn add_or_lookup_address_contact_id(&self, other: &TestContext) -> ContactId {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let primary_self_addr = other.sending_addr().await;
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
// origin when creating this contact.
|
||||
@@ -952,7 +971,7 @@ ORDER BY id"
|
||||
/// If the contact does not exist yet, a new contact will be created
|
||||
/// with the correct fingerprint, but without the public key.
|
||||
pub async fn add_or_lookup_contact_id_no_key(&self, other: &TestContext) -> ContactId {
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let primary_self_addr = other.sending_addr().await;
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
let fingerprint = self_fingerprint(other).await.unwrap();
|
||||
|
||||
@@ -1242,13 +1261,14 @@ ORDER BY id"
|
||||
}
|
||||
|
||||
pub async fn encrypt_raw_message(
|
||||
context: &Context,
|
||||
context: &TestContext,
|
||||
receivers: &[&TestContext],
|
||||
payload: &[u8],
|
||||
) -> Result<String> {
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let from = context.sending_addr().await;
|
||||
let aheader = Aheader {
|
||||
addr: context.get_primary_self_addr().await?,
|
||||
addr: from.clone(),
|
||||
public_key: public_key.clone(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
};
|
||||
@@ -1259,7 +1279,6 @@ pub async fn encrypt_raw_message(
|
||||
encryption_keyring.push(key::load_self_public_key(receiver).await?);
|
||||
}
|
||||
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let compress = false;
|
||||
|
||||
let mut cleartext = format!("Autocrypt: {aheader}").into_bytes();
|
||||
|
||||
+6
-8
@@ -20,13 +20,14 @@ use crate::test_utils::TestContextManager;
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let mut alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "Hi").await;
|
||||
let bob_alice_chat = bob.create_chat(&alice).await;
|
||||
|
||||
tcm.change_addr(&alice, "alice@someotherdomain.xyz").await;
|
||||
tcm.change_addr(&mut alice, "alice@someotherdomain.xyz")
|
||||
.await;
|
||||
|
||||
tcm.section("Bob sends a message to Alice, encrypting to her previous key");
|
||||
let sent = bob.send_text(bob_alice_chat.id, "hi back").await;
|
||||
@@ -63,7 +64,7 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
|
||||
const ALICE_NEW_ADDR: &str = "alice2@example.net";
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(alice, bob, "Hi").await;
|
||||
@@ -105,10 +106,7 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
|
||||
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
|
||||
|
||||
tcm.section("Test switching back");
|
||||
alice
|
||||
.set_primary_self_addr("alice@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
alice.set_sending_addr("alice@example.org").await;
|
||||
let sent = alice
|
||||
.send_text(chat_to_send, "Hello from my old addr!")
|
||||
.await;
|
||||
@@ -205,7 +203,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_write_to_alice_after_aeap() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_grp_id = chat::create_group(alice, "Group").await?;
|
||||
|
||||
+42
-59
@@ -276,14 +276,15 @@ impl ConfiguredLoginParam {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Loads legacy configured param. Only used for tests and the migration.
|
||||
/// Loads legacy configured param. Only used for tests and migration 131.
|
||||
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
|
||||
if !context.get_config_bool(Config::Configured).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.sql
|
||||
.get_raw_config("configured_addr")
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
@@ -480,11 +481,11 @@ pub(crate) async fn save_transport(
|
||||
);
|
||||
|
||||
let addr = addr_normalize(&configured.addr);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let mut modified = context
|
||||
let modified = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
||||
.transaction(|transaction| {
|
||||
let modified = transaction.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param, add_timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET entered_param=excluded.entered_param,
|
||||
@@ -493,24 +494,18 @@ pub(crate) async fn save_transport(
|
||||
WHERE entered_param != excluded.entered_param
|
||||
OR configured_param != excluded.configured_param
|
||||
OR add_timestamp < excluded.add_timestamp",
|
||||
(
|
||||
&addr,
|
||||
serde_json::to_string(entered_param)?,
|
||||
serde_json::to_string(configured)?,
|
||||
add_timestamp,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
> 0;
|
||||
(
|
||||
&addr,
|
||||
serde_json::to_string(entered_param)?,
|
||||
serde_json::to_string(configured)?,
|
||||
add_timestamp,
|
||||
),
|
||||
)? > 0;
|
||||
update_configured_addr(transaction)?;
|
||||
Ok(modified)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if configured_addr.is_none() {
|
||||
// If there is no transport yet, use the new transport for sending
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
modified = true;
|
||||
}
|
||||
Ok(modified)
|
||||
}
|
||||
|
||||
@@ -625,7 +620,7 @@ pub(crate) async fn sync_transports(
|
||||
)
|
||||
.collect();
|
||||
|
||||
let (deleted_ids, reelected) = context
|
||||
let deleted_ids = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut deleted_ids = Vec::new();
|
||||
@@ -636,10 +631,10 @@ pub(crate) async fn sync_transports(
|
||||
}
|
||||
deleted_ids.extend(delete_transport_row(transaction, addr, *timestamp)?);
|
||||
}
|
||||
update_configured_addr(transaction)?;
|
||||
modified |= !deleted_ids.is_empty();
|
||||
|
||||
let reelected = maybe_update_sending_transport(transaction)?;
|
||||
Ok((deleted_ids, reelected))
|
||||
Ok(deleted_ids)
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -647,12 +642,6 @@ pub(crate) async fn sync_transports(
|
||||
purge_transport_caches(context, *transport_id).await;
|
||||
}
|
||||
|
||||
if let Some(new_addr) = reelected {
|
||||
info!(context, "Re-elected sending transport {new_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if modified {
|
||||
context.self_public_key.lock().await.take();
|
||||
context
|
||||
@@ -711,34 +700,10 @@ pub(crate) async fn purge_transport_caches(context: &Context, transport_id: u32)
|
||||
context.metadata.write().await.remove(&transport_id);
|
||||
}
|
||||
|
||||
/// Elects another transport for sending if the current one vanished.
|
||||
/// Any remaining transport works and selection is anyway moving
|
||||
/// to the authority of the SMTP loop, see <https://github.com/chatmail/core/pull/8619>
|
||||
pub(crate) fn maybe_update_sending_transport(
|
||||
transaction: &mut rusqlite::Transaction,
|
||||
) -> Result<Option<String>> {
|
||||
let configured_addr: String = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let addrs = transport_addrs(transaction)?;
|
||||
if addrs.contains(&configured_addr) {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(new_addr) = addrs.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(&new_addr,),
|
||||
)?;
|
||||
Ok(Some(new_addr))
|
||||
}
|
||||
|
||||
/// Adds transport entry to the `transports` table with empty configuration.
|
||||
pub async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
|
||||
context.sql
|
||||
context.sql.transaction(|transaction| {
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
(
|
||||
@@ -746,8 +711,26 @@ pub async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
|
||||
serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
|
||||
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
|
||||
),
|
||||
).with_context(|| format!("Failed to insert pseudo transport for {addr:?}"))?;
|
||||
update_configured_addr(transaction)?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Update deprecated `configured_addr` config so it points to the first transport.
|
||||
///
|
||||
/// `configured_addr` is not used anymore,
|
||||
/// but old core (2026-11) fails when importing a backup without it,
|
||||
/// so we need to set it for backwards compatibility.
|
||||
pub fn update_configured_addr(transaction: &rusqlite::Transaction) -> Result<()> {
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value)
|
||||
SELECT 'configured_addr', addr
|
||||
FROM transports ORDER BY id LIMIT 1",
|
||||
(),
|
||||
)
|
||||
.await.with_context(|| format!("Failed to insert pseudo transport for {addr:?}"))?;
|
||||
.context("Failed to update configured_addr")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -197,10 +197,6 @@ async fn test_delete_transport() -> Result<()> {
|
||||
.evtracker
|
||||
.get_matching(|e| matches!(e, EventType::TransportsModified))
|
||||
.await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
|
||||
Some("alice@otherprovider.com")
|
||||
);
|
||||
sync_and_check_recipients(alice, alice2, "alice@otherprovider.com").await;
|
||||
|
||||
check_addrs(
|
||||
@@ -226,50 +222,6 @@ async fn test_delete_transport() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that selecting sending transport by setting "configured_addr" does not
|
||||
/// send the sync message, is not synchronized between devices even if sync message is forced,
|
||||
/// and does not bump sending transport `add_timestamp`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_configured_addr_synchronization() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
for a in [alice, alice2] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
a.set_config_bool(Config::BccSelf, true).await?;
|
||||
}
|
||||
|
||||
let addr = "alice@otherprovider.com";
|
||||
add_dummy_transport(alice, addr).await?;
|
||||
send_sync_transports(alice).await?;
|
||||
sync_and_check_recipients(alice, alice2, &format!("{addr} alice@example.org")).await;
|
||||
|
||||
// Selects `addr` on `alice` as the sending transport
|
||||
// and syncs the transport update to `alice2`,
|
||||
// whose own sending transport must stay unchanged.
|
||||
let old_timestamp = add_timestamp(alice2, addr).await;
|
||||
let alice2_primary = alice2.get_config(Config::ConfiguredAddr).await?;
|
||||
alice.set_config(Config::ConfiguredAddr, Some(addr)).await?;
|
||||
assert_eq!(add_timestamp(alice, addr).await, old_timestamp);
|
||||
|
||||
send_sync_transports(alice).await?;
|
||||
alice.send_sync_msg().await?.unwrap();
|
||||
let sync_msg = alice.pop_sent_msg().await;
|
||||
assert_eq!(sync_msg.recipients, format!("alice@example.org {addr}"));
|
||||
// The sync message comes from the new sending address,
|
||||
// which must not make `alice2` adopt it as its own sending address.
|
||||
assert!(sync_msg.payload.contains(&format!("From: <{addr}>")));
|
||||
alice2.recv_msg_trash(&sync_msg).await;
|
||||
|
||||
// add_timestamp must not change.
|
||||
assert_eq!(add_timestamp(alice2, addr).await, old_timestamp);
|
||||
assert_eq!(
|
||||
alice2.get_config(Config::ConfiguredAddr).await?,
|
||||
alice2_primary
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `sync_transports()` requests an IO restart
|
||||
/// if and only if it modified anything.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -288,14 +240,6 @@ async fn test_sync_transports_requests_io_restart() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_timestamp(t: &TestContext, addr: &str) -> i64 {
|
||||
t.sql
|
||||
.query_get_value("SELECT add_timestamp FROM transports WHERE addr=?", (addr,))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Tests that removing the last transport keeps it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_removing_last_transport() -> Result<()> {
|
||||
@@ -308,10 +252,6 @@ async fn test_removing_last_transport() -> Result<()> {
|
||||
};
|
||||
sync_transports(alice, &[], std::slice::from_ref(&removed)).await?;
|
||||
assert_eq!(alice.count_transports().await?, 1);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
|
||||
Some("alice@otherprovider.com")
|
||||
);
|
||||
|
||||
let removed = RemovedTransportData {
|
||||
addr: "alice@otherprovider.com".to_string(),
|
||||
@@ -319,37 +259,6 @@ async fn test_removing_last_transport() -> Result<()> {
|
||||
};
|
||||
sync_transports(alice, &[], std::slice::from_ref(&removed)).await?;
|
||||
assert_eq!(alice.count_transports().await?, 1);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ConfiguredAddr).await?.as_deref(),
|
||||
Some("alice@otherprovider.com")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests which transport is elected for sending.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_maybe_update_sending_transport() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
add_dummy_transport(t, "alice@one.com").await?;
|
||||
assert_eq!(
|
||||
t.sql.transaction(maybe_update_sending_transport).await?,
|
||||
None
|
||||
);
|
||||
|
||||
t.sql
|
||||
.execute(
|
||||
"DELETE FROM transports WHERE addr=?",
|
||||
("alice@example.org",),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.transaction(maybe_update_sending_transport)
|
||||
.await?
|
||||
.as_deref(),
|
||||
Some("alice@one.com")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
From: Bob <bob@example.net>
|
||||
Subject: Message with invalid HTML mime types
|
||||
To: alice@example.org
|
||||
Message-ID: <html-html-test@localhost>
|
||||
Date: Thu, 17 Dec 2020 15:38:45 +0100
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="------------MESSAGE_BOUNDARY"
|
||||
Content-Language: en-US
|
||||
Chat-Version: 1.0
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------MESSAGE_BOUNDARY
|
||||
Content-Type: html/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This should be ignored.
|
||||
|
||||
--------------MESSAGE_BOUNDARY
|
||||
Content-Type: html/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
And this should be ignored too.
|
||||
|
||||
--------------MESSAGE_BOUNDARY
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Here is a text part.
|
||||
|
||||
--------------MESSAGE_BOUNDARY--
|
||||
Reference in New Issue
Block a user