mirror of
https://github.com/chatmail/core.git
synced 2026-05-09 01:46:30 +03:00
Compare commits
1 Commits
dependabot
...
iequidoo/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08458500ac |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -23,7 +23,7 @@ env:
|
||||
RUST_VERSION: 1.95.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
MSRV: 1.88.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
|
||||
14
.github/workflows/deltachat-rpc-server.yml
vendored
14
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -105,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
345
Cargo.lock
generated
345
Cargo.lock
generated
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693"
|
||||
checksum = "3c23f3af104b40a3430ccb90ed5f7bd877a8dc5c26fc92fde51a22b40890dcf9"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
@@ -1386,7 +1386,7 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -2280,24 +2280,11 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.2.0",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
@@ -2591,15 +2578,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "human-panic"
|
||||
version = "2.0.8"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2815d0c49773c6e0a9753960b3d0e50b822e48e08c77bcb4780be8474c9cc3d"
|
||||
checksum = "075e8747af11abcff07d55d98297c9c6c70eb5d6365b25e7b12f02e484935191"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sysinfo 0.38.4",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"sysinfo 0.37.2",
|
||||
"toml",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2814,12 +2801,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idea"
|
||||
version = "0.5.1"
|
||||
@@ -2922,8 +2903,6 @@ checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3279,12 +3258,6 @@ dependencies = [
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
@@ -4582,16 +4555,6 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
@@ -4795,12 +4758,6 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -5505,9 +5462,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -5978,16 +5935,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.38.4"
|
||||
version = "0.37.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
|
||||
checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.62.1",
|
||||
"windows 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6314,18 +6271,6 @@ dependencies = [
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_writer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
@@ -6341,15 +6286,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.7"
|
||||
@@ -6390,9 +6326,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -6647,11 +6583,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.1"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -6709,24 +6645,6 @@ dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
@@ -6804,28 +6722,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.1"
|
||||
@@ -6839,18 +6735,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
@@ -6964,27 +6848,6 @@ dependencies = [
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49e6c4a1f363c8210c6f77ba24f645c61c6fb941eccf013da691f7e09515b8ac"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.62.1",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "123e712f464a8a60ce1a13f4c446d2d43ab06464cb5842ff68f5c71b6fb7852e"
|
||||
dependencies = [
|
||||
"windows-core 0.62.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -7014,36 +6877,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
|
||||
dependencies = [
|
||||
"windows-implement 0.59.0",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-interface 0.59.0",
|
||||
"windows-result 0.3.0",
|
||||
"windows-strings 0.3.0",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link",
|
||||
"windows-result 0.4.0",
|
||||
"windows-strings 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f3db6b24b120200d649cd4811b4947188ed3a8d2626f7075146c5d178a9a4a"
|
||||
dependencies = [
|
||||
"windows-core 0.62.1",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
@@ -7066,17 +6905,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
@@ -7090,9 +6918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7105,16 +6933,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8"
|
||||
dependencies = [
|
||||
"windows-core 0.62.1",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
@@ -7144,15 +6962,6 @@ dependencies = [
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
@@ -7172,15 +6981,6 @@ dependencies = [
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -7264,15 +7064,6 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -7439,32 +7230,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
@@ -7474,74 +7239,6 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wmi"
|
||||
version = "0.14.5"
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "deltachat"
|
||||
version = "2.50.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -103,7 +103,7 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.1", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
|
||||
13
STYLE.md
13
STYLE.md
@@ -161,16 +161,3 @@ are documented.
|
||||
|
||||
Follow Rust guidelines for the documentation comments:
|
||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||
|
||||
## Do not use `into()`, `try_into()` or `parse()`
|
||||
|
||||
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
|
||||
Instead, a `new()` function is recommended.
|
||||
|
||||
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
|
||||
over `into()`, `try_into()` or `parse()`.
|
||||
|
||||
Calling `into()`, `try_into()` or `parse()`
|
||||
creates an indirection,
|
||||
which is hard to follow for people who are not familiar with Rust,
|
||||
or who are not using rust-analyzer.
|
||||
|
||||
@@ -408,6 +408,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
@@ -416,7 +422,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* 1=delete messages directly after receiving from server, mvbox is skipped.
|
||||
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
|
||||
* "Saved messages" are deleted from the server as well as emails, the UI should clearly point that out.
|
||||
* "Saved messages" are deleted from the server as well as
|
||||
* e-mails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||
* See also dc_estimate_deletion_cnt().
|
||||
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||
@@ -534,6 +541,9 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
||||
* - `sys.config_keys` = get a space-separated list of all config-keys available.
|
||||
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
||||
* - `quota_exceeding` = 0: quota is unknown or in normal range;
|
||||
* >=80: quota is about to exceed, the value is the concrete percentage,
|
||||
* a device message is added when that happens, however, that value may still be interesting for bots.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object. For querying system values, this can be NULL.
|
||||
@@ -1387,6 +1397,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
#define DC_GCM_ADDDAYMARKER 0x01
|
||||
#define DC_GCM_INFO_ONLY 0x02
|
||||
|
||||
|
||||
/**
|
||||
@@ -1407,6 +1418,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
*/
|
||||
@@ -1470,6 +1482,7 @@ dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t ch
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* This includes e-mails downloaded due to the `show_emails` option.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
@@ -2815,6 +2828,19 @@ int dc_set_location (dc_context_t* context, double latit
|
||||
dc_array_t* dc_get_locations (dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int64_t timestamp_begin, int64_t timestamp_end);
|
||||
|
||||
|
||||
/**
|
||||
* Delete all locations on the current device.
|
||||
* Locations already sent cannot be deleted.
|
||||
*
|
||||
* Typically results in the event #DC_EVENT_LOCATION_CHANGED
|
||||
* with contact_id set to 0.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
*/
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
@@ -6400,7 +6426,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Location of one or more contact has changed.
|
||||
*
|
||||
* @param data1 (int) contact_id of the contact for which the location has changed.
|
||||
* If the locations of several contacts have been changed, this parameter is set to 0.
|
||||
* If the locations of several contacts have been changed,
|
||||
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_LOCATION_CHANGED 2035
|
||||
@@ -6671,6 +6698,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("show_emails")
|
||||
*/
|
||||
#define DC_SHOW_EMAILS_OFF 0
|
||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||
#define DC_SHOW_EMAILS_ALL 2
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("media_quality")
|
||||
*/
|
||||
@@ -7005,7 +7040,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in message summary text for notifications and chatlist.
|
||||
#define DC_STR_FORWARDED 97
|
||||
|
||||
/// @deprecated 2026-04-25
|
||||
/// "Quota exceeding, already %1$s%% used."
|
||||
///
|
||||
/// Used as device message text.
|
||||
///
|
||||
/// `%1$s` will be replaced by the percentage used
|
||||
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||
|
||||
/// "Multi Device Synchronization"
|
||||
|
||||
@@ -60,6 +60,7 @@ use self::string::*;
|
||||
// - finally, this behaviour matches the old core-c API and UIs already depend on it
|
||||
|
||||
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
|
||||
// dc_context_t
|
||||
|
||||
@@ -1337,13 +1338,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
|
||||
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs_ex(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
@@ -2541,7 +2546,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::send_to_chat(
|
||||
block_on(location::send_locations_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
@@ -2561,14 +2566,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
if chat_id == 0 {
|
||||
block_on(location::is_sending(ctx))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
block_on(location::is_sending_to_chat(ctx, ChatId::new(chat_id)))
|
||||
.unwrap_or_log_default(ctx, "Failed is_sending_locations_to_chat()")
|
||||
as libc::c_int
|
||||
}
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2584,9 +2589,12 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2621,6 +2629,23 @@ pub unsafe extern "C" fn dc_get_locations(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_delete_all_locations()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::delete_all(ctx)
|
||||
.await
|
||||
.context("Failed to delete locations")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
|
||||
@@ -318,6 +318,15 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Requests to clear storage on all chatmail relays.
|
||||
///
|
||||
/// I/O must be started for this request to take effect.
|
||||
async fn clear_all_relay_storage(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.clear_all_relay_storage().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get top-level info for an account.
|
||||
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
|
||||
let context_option = self.accounts.read().await.get_account(account_id);
|
||||
@@ -1366,22 +1375,8 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// Get all message IDs belonging to a chat.
|
||||
/// Returns all messages of a particular chat.
|
||||
///
|
||||
/// The list is already sorted and starts with the oldest message.
|
||||
/// Clients should not try to re-sort the list as this would be an expensive action
|
||||
/// and would result in inconsistencies between clients.
|
||||
/// Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
|
||||
/// UIs need to handle both the case of descending message IDs
|
||||
/// and of decreasing timestamps.
|
||||
///
|
||||
/// Optionally, 'daymarkers' added to the ID array may help to
|
||||
/// implement virtual lists.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * chat_id The chat ID of which the messages IDs should be queried.
|
||||
/// * _info_only: Deprecated, pass `false` here.
|
||||
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
/// corresponding (following) day in the local timezone.
|
||||
@@ -1389,14 +1384,17 @@ impl CommandApi {
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -1428,24 +1426,21 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all messages belonging to a chat.
|
||||
///
|
||||
/// Similar to `get_message_ids` / `getMessageIds`,
|
||||
/// see that function for details.
|
||||
/// The difference is that this function here returns a list of `MessageListItem`,
|
||||
/// which is an enum of a message or a daymarker.
|
||||
async fn get_message_list_items(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
_info_only: bool,
|
||||
info_only: bool,
|
||||
add_daymarker: bool,
|
||||
) -> Result<Vec<JsonrpcMessageListItem>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg = get_chat_msgs_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
MessageListOptions { add_daymarker },
|
||||
MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg
|
||||
@@ -1882,6 +1877,20 @@ impl CommandApi {
|
||||
deltachat::contact::make_vcard(&ctx, &contacts).await
|
||||
}
|
||||
|
||||
/// Sets vCard containing the given contacts to the message draft.
|
||||
async fn set_draft_vcard(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
contacts: Vec<u32>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
||||
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
msg.make_vcard(&ctx, &contacts).await?;
|
||||
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
@@ -2106,21 +2115,6 @@ impl CommandApi {
|
||||
// locations
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Sets current location.
|
||||
///
|
||||
/// Returns true if location streaming is currently
|
||||
/// enabled and locations should be updated.
|
||||
///
|
||||
/// Location is represented as latitude and longitude in degrees
|
||||
/// and horizontal accuracy in meters.
|
||||
async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.set_location(latitude, longitude, accuracy)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_locations(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2143,39 +2137,6 @@ impl CommandApi {
|
||||
Ok(locations.into_iter().map(|l| l.into()).collect())
|
||||
}
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
///
|
||||
/// Pass 0 as the number of seconds to disable location streaming in the chat.
|
||||
async fn send_locations_to_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::send_to_chat(&ctx, chat_id, seconds).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether any chat is sending locations.
|
||||
async fn is_sending_locations(&self, account_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
location::is_sending(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` is sending locations.
|
||||
async fn is_sending_locations_to_chat(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
location::is_sending_to_chat(&ctx, chat_id).await
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
async fn stop_sending_locations(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_sending_locations().await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// webxdc
|
||||
// ---------------------------------------------
|
||||
@@ -2407,7 +2368,6 @@ impl CommandApi {
|
||||
chat::resend_msgs(&ctx, &message_ids).await
|
||||
}
|
||||
|
||||
/// @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead.
|
||||
async fn send_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2419,6 +2379,9 @@ impl CommandApi {
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
|
||||
|
||||
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||
msg.force_sticker();
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
@@ -287,6 +287,8 @@ pub enum MessageViewtype {
|
||||
Gif,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
|
||||
@@ -238,7 +238,7 @@ impl From<Qr> for QrObject {
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
@@ -257,7 +257,7 @@ impl From<Qr> for QrObject {
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
@@ -278,7 +278,7 @@ impl From<Qr> for QrObject {
|
||||
is_v3,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
@@ -321,7 +321,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
@@ -338,7 +338,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
@@ -357,7 +357,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
@@ -374,7 +374,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
@@ -391,7 +391,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
@@ -410,7 +410,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.human_readable();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveJoinBroadcast {
|
||||
name,
|
||||
grpid,
|
||||
|
||||
@@ -345,6 +345,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chatinfo\n\
|
||||
sendlocations <seconds>\n\
|
||||
setlocation <lat> <lng>\n\
|
||||
dellocations\n\
|
||||
getlocations [<contact-id>]\n\
|
||||
send <text>\n\
|
||||
send-sync <text>\n\
|
||||
@@ -573,7 +574,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending(&context).await? {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
@@ -622,6 +623,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
sel_chat.get_id(),
|
||||
chat::MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: true,
|
||||
},
|
||||
)
|
||||
@@ -780,7 +782,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
println!(
|
||||
"Location streaming: {}",
|
||||
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -820,7 +826,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||
|
||||
let seconds = arg1.parse()?;
|
||||
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
|
||||
location::send_locations_to_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -842,6 +853,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Success, streaming can be stopped.");
|
||||
}
|
||||
}
|
||||
"dellocations" => {
|
||||
location::delete_all(&context).await?;
|
||||
}
|
||||
"send" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No message text given.");
|
||||
|
||||
@@ -176,7 +176,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 40] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -194,6 +194,7 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"chatinfo",
|
||||
"sendlocations",
|
||||
"setlocation",
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"send-sync",
|
||||
|
||||
@@ -13,7 +13,7 @@ def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
@@ -21,30 +21,36 @@ def main():
|
||||
account.set_config("bot", "1")
|
||||
if not account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
|
||||
account.set_config("addr", sys.argv[1])
|
||||
account.set_config("mail_pw", sys.argv[2])
|
||||
account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
deltachat.start_io()
|
||||
|
||||
qr = account.get_qr_code()
|
||||
logging.info(f"Invite link: {qr}")
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.INFO:
|
||||
logging.info(event["msg"])
|
||||
elif event.kind == EventType.WARNING:
|
||||
logging.warning(event["msg"])
|
||||
elif event.kind == EventType.ERROR:
|
||||
logging.error(event["msg"])
|
||||
elif event.kind == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
message = account.get_message_by_id(event.msg_id)
|
||||
def process_messages():
|
||||
for message in account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
process_messages()
|
||||
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event["kind"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["kind"] == EventType.WARNING:
|
||||
logging.warning("%s", event["msg"])
|
||||
elif event["kind"] == EventType.ERROR:
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["kind"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .chat import Chat
|
||||
@@ -391,7 +392,8 @@ class Account:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, process "incoming message" events instead.
|
||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
@@ -461,6 +463,16 @@ class Account:
|
||||
"""Wait for reaction change event."""
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
@@ -483,7 +495,3 @@ class Account:
|
||||
"""Return ICE servers for WebRTC configuration."""
|
||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||
return json.loads(ice_servers_json)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to any chat."""
|
||||
return self._rpc.is_sending_locations(self.id)
|
||||
|
||||
@@ -164,7 +164,7 @@ class Chat:
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_sticker(self, path: str) -> Message:
|
||||
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
|
||||
"""Send an sticker and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
@@ -206,9 +206,9 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""Get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
@@ -277,16 +277,6 @@ class Chat:
|
||||
"""Remove profile image of this chat."""
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
def send_locations(self, seconds) -> None:
|
||||
"""Enable location streaming in the chat for the given number of seconds.
|
||||
|
||||
Pass 0 to disable location streaming."""
|
||||
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
|
||||
|
||||
def is_sending_locations(self) -> bool:
|
||||
"""Return True if sending locations to this chat."""
|
||||
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
|
||||
|
||||
def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
|
||||
@@ -59,11 +59,3 @@ class DeltaChat:
|
||||
def set_translations(self, translations: dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
def set_location(self, latitude, longitude, accuracy) -> bool:
|
||||
"""Set location, return True if location streaming should continue."""
|
||||
return self.rpc.set_location(latitude, longitude, accuracy)
|
||||
|
||||
def stop_sending_locations(self) -> None:
|
||||
"""Stop sending locations to all chats."""
|
||||
return self.rpc.stop_sending_locations()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
def test_set_location(dc, acfactory) -> None:
|
||||
# Try setting location without any accounts.
|
||||
assert not dc.set_location(1.0, 2.0, 0.1)
|
||||
|
||||
# Create one account that does not stream,
|
||||
# set location.
|
||||
acfactory.new_configured_account()
|
||||
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||
|
||||
|
||||
def test_send_locations_to_chat(dc, acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
assert not alice.is_sending_locations()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test starting and stopping location streaming in a chat.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
alice_chat_bob.send_locations(0)
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
|
||||
# Test stop_sending_locations() for all accounts and chats.
|
||||
alice_chat_bob.send_locations(3600)
|
||||
assert alice.is_sending_locations()
|
||||
assert alice_chat_bob.is_sending_locations()
|
||||
dc.stop_sending_locations()
|
||||
assert not alice.is_sending_locations()
|
||||
assert not alice_chat_bob.is_sending_locations()
|
||||
@@ -9,6 +9,8 @@ def test_add_second_address(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.add_transport_from_qr(qr)
|
||||
assert len(account.list_transports()) == 2
|
||||
@@ -26,6 +28,22 @@ def test_add_second_address(acfactory) -> None:
|
||||
account.delete_transport(second_addr)
|
||||
assert len(account.list_transports()) == 2
|
||||
|
||||
# show_emails does not matter for multi-relay, can be set to anything
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
|
||||
def test_second_transport_without_classic_emails(acfactory) -> None:
|
||||
"""Test that second transport can be configured if classic emails are not fetched."""
|
||||
account = acfactory.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
assert account.get_config("show_emails") == "2"
|
||||
|
||||
qr = acfactory.get_account_qr()
|
||||
account.set_config("show_emails", "0")
|
||||
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_change_address(acfactory) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future;
|
||||
use futures_lite::FutureExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
@@ -23,7 +22,6 @@ use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::location;
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
@@ -538,38 +536,6 @@ impl Accounts {
|
||||
self.push_subscriber.set_device_token(token).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets location for all accounts.
|
||||
///
|
||||
/// Returns true if location should still be streamed.
|
||||
pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
let continue_streaming = future::try_join_all(self.accounts.iter().map(
|
||||
|(account_id, account)| async move {
|
||||
location::set(account, latitude, longitude, accuracy)
|
||||
.await
|
||||
.with_context(|| format!("Failed to set location for account {account_id}"))
|
||||
},
|
||||
))
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|continue_streaming| continue_streaming);
|
||||
Ok(continue_streaming)
|
||||
}
|
||||
|
||||
/// Stops sending locations to all chats.
|
||||
pub async fn stop_sending_locations(&self) -> Result<()> {
|
||||
future::try_join_all(
|
||||
self.accounts
|
||||
.iter()
|
||||
.map(|(account_id, account)| async move {
|
||||
location::stop_sending(account).await.with_context(|| {
|
||||
format!("Failed to stop sending locations for account {account_id}")
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
@@ -27,8 +28,10 @@ impl fmt::Display for EncryptPreference {
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptPreference {
|
||||
fn new(s: &str) -> Result<Self> {
|
||||
impl FromStr for EncryptPreference {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
@@ -82,8 +85,10 @@ impl fmt::Display for Aheader {
|
||||
}
|
||||
}
|
||||
|
||||
impl Aheader {
|
||||
pub(crate) fn from_str(s: &str) -> Result<Self> {
|
||||
impl FromStr for Aheader {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut attributes: BTreeMap<String, String> = s
|
||||
.split(';')
|
||||
.filter_map(|a| {
|
||||
@@ -111,7 +116,7 @@ impl Aheader {
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
.and_then(|raw| EncryptPreference::new(&raw).ok())
|
||||
.and_then(|raw| raw.parse().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
@@ -139,9 +144,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_from_str() -> Result<()> {
|
||||
let h = Aheader::from_str(&format!(
|
||||
"addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}"
|
||||
))?;
|
||||
let h: Aheader =
|
||||
format!("addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}").parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
@@ -153,7 +157,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
|
||||
let h = Aheader::from_str(&raw)?;
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "reset@example.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
@@ -163,7 +167,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_from_str_non_critical() -> Result<()> {
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}");
|
||||
let h = Aheader::from_str(&raw)?;
|
||||
let h: Aheader = raw.parse()?;
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
@@ -173,7 +177,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_from_str_superflous_critical() {
|
||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}");
|
||||
assert!(Aheader::from_str(&raw).is_err());
|
||||
assert!(raw.parse::<Aheader>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
561
src/authres.rs
Normal file
561
src/authres.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
//! Parsing and handling of the Authentication-Results header.
|
||||
//! See the comment on [`handle_authres`] for more.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
/// about whether DKIM and SPF passed.
|
||||
///
|
||||
/// To mitigate From forgery, we remember for each sending domain whether it is known
|
||||
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
|
||||
/// we don't allow changing the autocrypt key.
|
||||
///
|
||||
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
|
||||
pub(crate) async fn handle_authres(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres).await
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for DkimResults {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type AuthservId = String;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DkimResult {
|
||||
/// The header explicitly said that DKIM passed
|
||||
Passed,
|
||||
/// The header explicitly said that DKIM failed
|
||||
Failed,
|
||||
/// The header didn't say anything about DKIM; this might mean that it wasn't
|
||||
/// checked, but it might also mean that it failed. This is because some providers
|
||||
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
|
||||
/// Authentication-Results if there was no DKIM.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
|
||||
|
||||
fn parse_authres_headers(
|
||||
headers: &mailparse::headers::Headers<'_>,
|
||||
from_domain: &str,
|
||||
) -> ParsedAuthresHeaders {
|
||||
let mut res = Vec::new();
|
||||
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
||||
let header_value = remove_comments(&header_value);
|
||||
|
||||
if let Some(mut authserv_id) = header_value.split(';').next() {
|
||||
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
|
||||
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
|
||||
// because there is whitespace in the first identifier before the ';'.
|
||||
// Authentication-Results-parsing still works securely because they remove incoming
|
||||
// Authentication-Results headers.
|
||||
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
|
||||
// with providers not implementing the RFC correctly, someone can trick us
|
||||
// into thinking that an incoming email is DKIM-correct, anyway.
|
||||
// The most important thing here is that we have some valid `authserv_id`.
|
||||
authserv_id = "invalidAuthservId";
|
||||
}
|
||||
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
|
||||
res.push((authserv_id.to_string(), dkim_passed));
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// The headers can contain comments that look like this:
|
||||
/// ```text
|
||||
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
|
||||
/// ```
|
||||
fn remove_comments(header: &str) -> Cow<'_, str> {
|
||||
// In Pomsky, this is:
|
||||
// "(" Codepoint* lazy ")"
|
||||
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
|
||||
static RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
|
||||
RE.replace_all(header, " ")
|
||||
}
|
||||
|
||||
/// Parses a single Authentication-Results header, like:
|
||||
///
|
||||
/// ```text
|
||||
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
/// ```
|
||||
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
|
||||
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
|
||||
// Check that the character right before `dkim=` is a space or a tab
|
||||
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
|
||||
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
|
||||
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
|
||||
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
||||
if let Some(&"pass") = dkim_parts.first() {
|
||||
// DKIM headers contain a header.d or header.i field
|
||||
// that says which domain signed. We have to check ourselves
|
||||
// that this is the same domain as in the From header.
|
||||
let header_d: &str = &format!("header.d={}", &from_domain);
|
||||
let header_i: &str = &format!("header.i=@{}", &from_domain);
|
||||
|
||||
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
||||
// We have found a `dkim=pass` header!
|
||||
return DkimResult::Passed;
|
||||
}
|
||||
} else {
|
||||
// dkim=fail, dkim=none, ...
|
||||
return DkimResult::Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DkimResult::Nothing
|
||||
}
|
||||
|
||||
/// ## About authserv-ids
|
||||
///
|
||||
/// After having checked DKIM, our email server adds an Authentication-Results header.
|
||||
///
|
||||
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
|
||||
/// in order to make us think that DKIM was correct in their From-forged email.
|
||||
///
|
||||
/// In order to prevent this, each email server adds its authserv-id to the
|
||||
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
|
||||
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
|
||||
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
|
||||
///
|
||||
/// We need to somehow find out the authserv-id(s) of our email server, so that
|
||||
/// we can use the Authentication-Results with the right authserv-id.
|
||||
///
|
||||
/// ## What this function does
|
||||
///
|
||||
/// When receiving an email, this function is called and updates the candidates for
|
||||
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
|
||||
///
|
||||
/// Usually, every incoming email has Authentication-Results with our server's
|
||||
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
|
||||
/// authserv-ids for our server's authserv-id is a good guess for our server's
|
||||
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
|
||||
/// changed and start over with the new authserv-ids.
|
||||
///
|
||||
/// See [`handle_authres`].
|
||||
async fn update_authservid_candidates(
|
||||
context: &Context,
|
||||
authres: &ParsedAuthresHeaders,
|
||||
) -> Result<()> {
|
||||
let mut new_ids: BTreeSet<&str> = authres
|
||||
.iter()
|
||||
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
|
||||
.collect();
|
||||
if new_ids.is_empty() {
|
||||
// The incoming message doesn't contain any authentication results, maybe it's a
|
||||
// self-sent or a mailer-daemon message
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let old_ids = parse_authservid_candidates_config(&old_config);
|
||||
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
|
||||
if !intersection.is_empty() {
|
||||
new_ids = intersection;
|
||||
}
|
||||
// If there were no AuthservIdCandidates previously, just start with
|
||||
// the ones from the incoming email
|
||||
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
|
||||
/// and whether a keychange should be allowed.
|
||||
///
|
||||
/// We track in the `sending_domains` table whether we get positive Authentication-Results
|
||||
/// for mails from a contact (meaning that their provider properly authenticates against
|
||||
/// our provider).
|
||||
///
|
||||
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
|
||||
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
|
||||
async fn compute_dkim_results(
|
||||
context: &Context,
|
||||
mut authres: ParsedAuthresHeaders,
|
||||
) -> Result<DkimResults> {
|
||||
let mut dkim_passed = false;
|
||||
|
||||
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let ids = parse_authservid_candidates_config(&ids_config);
|
||||
|
||||
// Remove all foreign authentication results
|
||||
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
|
||||
|
||||
if authres.is_empty() {
|
||||
// If the authentication results are empty, then our provider doesn't add them
|
||||
// and an attacker could just add their own Authentication-Results, making us
|
||||
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
|
||||
dkim_passed = true;
|
||||
} else {
|
||||
for (_authserv_id, current_dkim_passed) in authres {
|
||||
match current_dkim_passed {
|
||||
DkimResult::Passed => {
|
||||
dkim_passed = true;
|
||||
break;
|
||||
}
|
||||
DkimResult::Failed => {
|
||||
dkim_passed = false;
|
||||
break;
|
||||
}
|
||||
DkimResult::Nothing => {
|
||||
// Continue looking for an Authentication-Results header
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DkimResults { dkim_passed })
|
||||
}
|
||||
|
||||
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
|
||||
config
|
||||
.as_deref()
|
||||
.map(|c| c.split_whitespace().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
|
||||
#[test]
|
||||
fn test_remove_comments() {
|
||||
let header = "Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
remove_comments(&header),
|
||||
"Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass header.d=riseup.net;"
|
||||
);
|
||||
|
||||
let header = ") aaa (".to_string();
|
||||
assert_eq!(remove_comments(&header), ") aaa (");
|
||||
|
||||
let header = "((something weird) no comment".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment");
|
||||
|
||||
let header = "🎉(🎉(🎉))🎉(".to_string();
|
||||
assert_eq!(remove_comments(&header), "🎉 )🎉(");
|
||||
|
||||
// Comments are allowed to include whitespace
|
||||
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment ");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_authentication_results() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@gmx.net").await;
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Passed),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Nothing),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
|
||||
|
||||
// Weird Authentication-Results from Outlook without an authserv-id
|
||||
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
|
||||
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
|
||||
header.d=hotmail.com;dmarc=pass action=none
|
||||
header.from=hotmail.com;compauth=pass reason=100";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
|
||||
// At this point, the most important thing to test is that there are no
|
||||
// authserv-ids with whitespace in them.
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Failed),
|
||||
("gmx.net".to_string(), DkimResult::Passed)
|
||||
]
|
||||
);
|
||||
|
||||
// ';' in comments
|
||||
let bytes = b"Authentication-Results: mx1.riseup.net;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
|
||||
dkim-atps=neutral";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = br#"Authentication-Results: box.hispanilandia.net;
|
||||
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Failed),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_authservid_candidates() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx3.messagingengine.com");
|
||||
|
||||
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
// A message without any Authentication-Results headers shouldn't remove all
|
||||
// candidates since it could be a mailer-daemon message or so
|
||||
update_authservid_candidates_test(&t, &[]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
|
||||
.await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls update_authservid_candidates(), meant for using in a test.
|
||||
///
|
||||
/// update_authservid_candidates() only looks at the keys of its
|
||||
/// `authentication_results` parameter. So, this function takes `incoming_ids`
|
||||
/// and adds some AuthenticationResults to get the HashMap we need.
|
||||
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
|
||||
let v = incoming_ids
|
||||
.iter()
|
||||
.map(|id| (id.to_string(), DkimResult::Passed))
|
||||
.collect();
|
||||
update_authservid_candidates(context, &v).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_realworld_authentication_results() -> Result<()> {
|
||||
let mut test_failed = false;
|
||||
|
||||
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
for entry in dir {
|
||||
if !entry.file_type().await.unwrap().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let self_addr = entry.file_name().into_string().unwrap();
|
||||
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
|
||||
let authres_parsing_works = [
|
||||
"ik.me",
|
||||
"web.de",
|
||||
"posteo.de",
|
||||
"gmail.com",
|
||||
"hotmail.com",
|
||||
"mail.ru",
|
||||
"aol.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"fastmail.com",
|
||||
"mail.de",
|
||||
"outlook.com",
|
||||
"gmx.de",
|
||||
"testrun.org",
|
||||
]
|
||||
.contains(&self_domain.as_str());
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr(&self_addr).await;
|
||||
if !authres_parsing_works {
|
||||
println!("========= Receiving as {} =========", &self_addr);
|
||||
}
|
||||
|
||||
// Simulate receiving all emails once, so that we have the correct authserv-ids
|
||||
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
|
||||
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from).await?;
|
||||
let from_domain = EmailAddress::new(from).unwrap().domain;
|
||||
|
||||
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
|
||||
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
|
||||
// These are (fictional) forged emails where the attacker added a fake
|
||||
// Authentication-Results before sending the email
|
||||
&& from != "forged-authres-added@example.com"
|
||||
// Other forged emails
|
||||
&& !from.starts_with("forged");
|
||||
|
||||
if res.dkim_passed != expected_result {
|
||||
if authres_parsing_works {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
|
||||
entry.path(),
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
println!("From {}: {}", from_domain, res.dkim_passed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!test_failed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"From: invalid@from.com
|
||||
Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob knows his server's authserv-id
|
||||
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
|
||||
.await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
|
||||
sent.payload
|
||||
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.error.is_none());
|
||||
|
||||
// Do the same without the mailing list header, this time the failed
|
||||
// authres isn't ignored
|
||||
let mut sent = alice
|
||||
.send_text(alice_bob_chat.id, "hellooo without mailing list")
|
||||
.await;
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
// The message info should contain a warning:
|
||||
assert!(
|
||||
rcvd.id
|
||||
.get_info(&bob)
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("DKIM Results: Passed=false")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -284,6 +284,10 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
|
||||
/// image, `*viewtype` is set to [`Viewtype::Image`].
|
||||
///
|
||||
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
|
||||
/// image is a true sticker assuming that it must have at least one fully transparent corner,
|
||||
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
|
||||
pub async fn check_or_recode_image(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
|
||||
@@ -445,6 +445,7 @@ async fn test_recode_image_balanced_png() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
@@ -452,7 +453,6 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
res_viewtype: Some(Viewtype::Sticker),
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
@@ -734,6 +734,8 @@ async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||
// extension.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
199
src/chat.rs
199
src/chat.rs
@@ -6,6 +6,7 @@ use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::marker::Sync;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
@@ -1210,8 +1211,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
);
|
||||
let fingerprint = contact
|
||||
.fingerprint()
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?
|
||||
.human_readable();
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?;
|
||||
if let Some(public_key) = contact.public_key(context).await? {
|
||||
if let Some(relay_addrs) = addresses_from_public_key(&public_key) {
|
||||
let relays = relay_addrs.join(",");
|
||||
@@ -2467,7 +2467,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let mut maybe_image = false;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::File
|
||||
|| msg.viewtype == Viewtype::Image
|
||||
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
|
||||
{
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
@@ -2475,7 +2478,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
|
||||
if better_type == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::Sticker {
|
||||
if better_type != Viewtype::Image {
|
||||
// UIs don't want conversions of `Sticker` to anything other than `Image`.
|
||||
msg.param.set_int(Param::ForceSticker, 1);
|
||||
}
|
||||
} else if better_type == Viewtype::Image {
|
||||
maybe_image = true;
|
||||
} else if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
@@ -2495,7 +2503,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::File && maybe_image
|
||||
|| msg.viewtype == Viewtype::Image
|
||||
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
|
||||
{
|
||||
let new_name = blob
|
||||
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
|
||||
.await?;
|
||||
@@ -2742,8 +2753,8 @@ async fn prepare_send_msg(
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
let resend_to_new = false;
|
||||
let row_ids = create_send_msg_jobs(context, msg, resend_to_new)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
if !row_ids.is_empty() {
|
||||
@@ -2813,7 +2824,11 @@ async fn render_mime_message_and_pre_message(
|
||||
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
async fn create_send_msg_jobs(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
resend_to_new: bool,
|
||||
) -> Result<Vec<i64>> {
|
||||
let cmd = msg.param.get_cmd();
|
||||
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
||||
msg.chat_id
|
||||
@@ -2929,35 +2944,41 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26,28) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if !resend_to_new {
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26,28) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
let msg_id = match resend_to_new {
|
||||
true => MsgId::new(u32::MAX),
|
||||
false => msg.id,
|
||||
};
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
|
||||
@@ -2978,7 +2999,7 @@ WHERE id=?
|
||||
&pre_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&pre_msg.message,
|
||||
msg.id,
|
||||
msg_id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
@@ -2986,7 +3007,7 @@ WHERE id=?
|
||||
&rendered_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
msg_id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
@@ -3097,6 +3118,9 @@ async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
/// Chat message list request options.
|
||||
#[derive(Debug)]
|
||||
pub struct MessageListOptions {
|
||||
/// Return only info messages.
|
||||
pub info_only: bool,
|
||||
|
||||
/// Add day markers before each date regarding the local timezone.
|
||||
pub add_daymarker: bool,
|
||||
}
|
||||
@@ -3107,6 +3131,7 @@ pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<Cha
|
||||
context,
|
||||
chat_id,
|
||||
MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: false,
|
||||
},
|
||||
)
|
||||
@@ -3121,13 +3146,43 @@ pub async fn get_chat_msgs_ex(
|
||||
chat_id: ChatId,
|
||||
options: MessageListOptions,
|
||||
) -> Result<Vec<ChatItem>> {
|
||||
let MessageListOptions { add_daymarker } = options;
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
false,
|
||||
))
|
||||
let MessageListOptions {
|
||||
info_only,
|
||||
add_daymarker,
|
||||
} = options;
|
||||
// TODO: Remove `info_only` parameter; it's not used by anything
|
||||
let process_row = if info_only {
|
||||
|row: &rusqlite::Row| {
|
||||
// is_info logic taken from Message.is_info()
|
||||
let params = row.get::<_, String>("param")?;
|
||||
let (from_id, to_id) = (
|
||||
row.get::<_, ContactId>("from_id")?,
|
||||
row.get::<_, ContactId>("to_id")?,
|
||||
);
|
||||
let is_info_msg: bool = from_id == ContactId::INFO
|
||||
|| to_id == ContactId::INFO
|
||||
|| match Params::from_str(¶ms) {
|
||||
Ok(p) => {
|
||||
let cmd = p.get_cmd();
|
||||
cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
!is_info_msg,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
|row: &rusqlite::Row| {
|
||||
Ok((
|
||||
row.get::<_, i64>("timestamp")?,
|
||||
row.get::<_, MsgId>("id")?,
|
||||
false,
|
||||
))
|
||||
}
|
||||
};
|
||||
let process_rows = |rows: rusqlite::AndThenRows<_>| {
|
||||
// It is faster to sort here rather than
|
||||
@@ -3162,18 +3217,39 @@ pub async fn get_chat_msgs_ex(
|
||||
Ok(ret)
|
||||
};
|
||||
|
||||
let items = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
let items = if info_only {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
// GLOB is used here instead of LIKE because it is case-sensitive
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND (
|
||||
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
(chat_id, ContactId::INFO, ContactId::INFO),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0;",
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?;
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
@@ -4543,7 +4619,11 @@ pub async fn forward_msgs_2ctx(
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
||||
let resend_to_new = false;
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg, resend_to_new)
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
ctx_dst.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
created_msgs.push(msg.id);
|
||||
@@ -4701,7 +4781,11 @@ pub(crate) async fn resend_msgs_ex(
|
||||
if let Some(to_fingerprint) = &to_fingerprint {
|
||||
msg.param.set(Param::Arg4, to_fingerprint.clone());
|
||||
}
|
||||
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
let resend_to_new = to_fingerprint.is_some();
|
||||
if create_send_msg_jobs(context, &mut msg, resend_to_new)
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4906,6 +4990,8 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
// no wrong information are shown in the device chat
|
||||
// - deletion in `devmsglabels` makes sure,
|
||||
// deleted messages are reset and useful messages can be added again
|
||||
// - we reset the config-option `QuotaExceeding`
|
||||
// that is used as a helper to drive the corresponding device message.
|
||||
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
@@ -4921,6 +5007,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2032,6 +2032,12 @@ async fn test_classic_email_chat() -> Result<()> {
|
||||
let msgs = get_chat_msgs(&alice, chat_id).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// Alice disables receiving classic emails.
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Already received classic email should still be in the chat.
|
||||
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
|
||||
@@ -2069,7 +2075,13 @@ async fn test_chat_get_color_encrypted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
async fn test_sticker(
|
||||
filename: &str,
|
||||
bytes: &[u8],
|
||||
res_viewtype: Viewtype,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
@@ -2085,7 +2097,7 @@ async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(msg.get_filename().unwrap(), filename);
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
@@ -2099,6 +2111,7 @@ async fn test_sticker_png() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.png",
|
||||
include_bytes!("../../test-data/image/logo.png"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
@@ -2110,6 +2123,7 @@ async fn test_sticker_jpeg() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../../test-data/image/avatar1000x1000.jpg"),
|
||||
Viewtype::Image,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
@@ -2117,33 +2131,10 @@ async fn test_sticker_jpeg() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../../test-data/image/logo.gif"),
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Tests that stickers are sent as stickers.
|
||||
///
|
||||
/// Previously there was heuristic that stickers
|
||||
/// were sometimes turned into non-stickers,
|
||||
/// e.g. when it looked like UI sent
|
||||
/// a screenshot dragged from the gallery into chat
|
||||
/// as a sticker.
|
||||
///
|
||||
/// We have no such heuristic anymore,
|
||||
/// if such heuristic is needed on some platform,
|
||||
/// UI code should implement it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_no_heuristics() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
async fn test_sticker_jpeg_force() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
let file = alice.get_blobdir().join("sticker.jpg");
|
||||
tokio::fs::write(
|
||||
@@ -2153,38 +2144,53 @@ async fn test_sticker_no_heuristics() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a sticker.
|
||||
// Images without force_sticker should be turned into [Viewtype::Image]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
let file = msg.get_file(alice).unwrap();
|
||||
let file = msg.get_file(&alice).unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Image);
|
||||
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
msg.force_sticker();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
|
||||
// Send a sticker reusing the file.
|
||||
// Images with `force_sticker = true` should keep [Viewtype::Sticker]
|
||||
// even on drafted messages
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
|
||||
// Set sticker as a draft, then send it.
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, Some("sticker.jpg"), None)
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("sticker.jpg"), None)
|
||||
.unwrap();
|
||||
msg.force_sticker();
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(alice, Some(&mut msg))
|
||||
.set_draft(&alice, Some(&mut msg))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut msg = alice_chat.id.get_draft(alice).await.unwrap().unwrap();
|
||||
let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../../test-data/image/logo.gif"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sticker_forward() -> Result<()> {
|
||||
// create chats
|
||||
@@ -3043,6 +3049,31 @@ async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_resend_failed_msg_to_new_member() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_bc_id = create_broadcast(alice, "bc".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
|
||||
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let alice_msg_id = alice.send_text(alice_bc_id, "text").await.sender_msg_id;
|
||||
let mut msg = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
message::set_msg_failed(alice, &mut msg, "error").await?;
|
||||
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
let resent_msg = alice.pop_sent_msg().await;
|
||||
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||
assert_eq!(fiona_msg.text, "text");
|
||||
assert_eq!(
|
||||
alice_msg_id.get_state(alice).await?,
|
||||
MessageState::OutFailed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Alice has multiple devices
|
||||
/// - Alice creates a broadcast and sends a message into it
|
||||
/// - Alice's second device sees the broadcast
|
||||
@@ -5816,7 +5847,7 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives both messages and has nothing at the end
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
|
||||
@@ -5824,11 +5855,6 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// ... even if he receives messages in reverse order.
|
||||
let bob2 = &tcm.bob().await;
|
||||
bob2.recv_msg_opt(&sent2).await;
|
||||
assert!(bob2.recv_msg_opt(&sent1).await.is_none());
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
|
||||
@@ -190,6 +190,10 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
/// Whether to show classic emails or only chat messages.
|
||||
#[strum(props(default = "2"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
/// Quality of the media files to send.
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
@@ -367,6 +371,11 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// If a warning about exceeding quota was shown recently,
|
||||
/// this is the percentage of quota at the time the warning was given.
|
||||
/// Unset, when quota falls below minimal warning threshold again.
|
||||
QuotaExceeding,
|
||||
|
||||
/// Timestamp of the last time housekeeping was run
|
||||
LastHousekeeping,
|
||||
|
||||
@@ -407,6 +416,15 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
/// may be the one of our email server.
|
||||
///
|
||||
/// See `crate::authres::update_authservid_candidates`.
|
||||
AuthservIdCandidates,
|
||||
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
/// Let the core save all events to the database.
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -501,7 +519,11 @@ impl Config {
|
||||
pub(crate) fn is_synced(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -707,6 +729,7 @@ impl Context {
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
| Config::SyncMsgs
|
||||
| Config::SignUnencrypted
|
||||
| Config::DisableIdle => {
|
||||
ensure!(
|
||||
matches!(value, None | Some("0") | Some("1")),
|
||||
@@ -940,23 +963,16 @@ 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_all_self_addrs()
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
.any(|a| addr_cmp(addr, a))
|
||||
|| self
|
||||
.get_secondary_self_addrs()
|
||||
.await?
|
||||
.iter()
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the new primary self address and saves the old
|
||||
@@ -1003,6 +1019,14 @@ impl Context {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all secondary self addresses.
|
||||
pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Returns all published secondary self addresses.
|
||||
/// See `[Context::set_transport_unpublished]`
|
||||
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
||||
|
||||
@@ -196,6 +196,13 @@ async fn test_sync() -> Result<()> {
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
|
||||
|
||||
{
|
||||
let val = alice0.get_config_bool(Config::ShowEmails).await?;
|
||||
alice0.set_config_bool(Config::ShowEmails, !val).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(Config::ShowEmails).await?, !val);
|
||||
}
|
||||
|
||||
// `Config::SyncMsgs` mustn't be synced.
|
||||
alice0.set_config_bool(Config::SyncMsgs, false).await?;
|
||||
alice0.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
@@ -36,6 +36,17 @@ pub enum Blocked {
|
||||
Request = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
AcceptedContacts = 1,
|
||||
#[default] // also change Config.ShowEmails props(default) on changes
|
||||
All = 2,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
@@ -251,6 +262,18 @@ mod tests {
|
||||
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(ShowEmails::All, ShowEmails::default());
|
||||
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
ShowEmails::AcceptedContacts,
|
||||
ShowEmails::from_i32(1).unwrap()
|
||||
);
|
||||
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blocked_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
@@ -1396,7 +1396,7 @@ WHERE addr=?
|
||||
let Some(fingerprint_other) = contact.fingerprint() else {
|
||||
return Ok(stock_str::encr_none(context));
|
||||
};
|
||||
let fingerprint_other = fingerprint_other.human_readable();
|
||||
let fingerprint_other = fingerprint_other.to_string();
|
||||
|
||||
let stock_message = if contact.public_key(context).await?.is_some() {
|
||||
stock_str::messages_are_e2ee(context)
|
||||
@@ -1410,7 +1410,7 @@ WHERE addr=?
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.dc_fingerprint()
|
||||
.human_readable();
|
||||
.to_string();
|
||||
if addr < contact.addr {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
|
||||
@@ -568,6 +568,15 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests deletion of all messages from chatmail relays.
|
||||
///
|
||||
/// Non-chatmail relays are excluded
|
||||
/// to avoid accidentally deleting emails
|
||||
/// from shared inboxes.
|
||||
pub async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
self.scheduler.clear_all_relay_storage().await
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
/// when it is not running this is an no-op
|
||||
pub async fn restart_io_if_running(&self) {
|
||||
@@ -843,7 +852,7 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let all_self_addrs = self.get_all_self_addrs().await?.join(", ");
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -945,7 +954,11 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
res.insert("all_self_addrs", all_self_addrs);
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"who_can_call_me",
|
||||
self.get_config_int(Config::WhoCanCallMe).await?.to_string(),
|
||||
@@ -991,6 +1004,24 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"quota_exceeding",
|
||||
self.get_config_int(Config::QuotaExceeding)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"authserv_id_candidates",
|
||||
self.get_config(Config::AuthservIdCandidates)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
res.insert(
|
||||
"sign_unencrypted",
|
||||
self.get_config_int(Config::SignUnencrypted)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
|
||||
@@ -602,7 +602,10 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(alice.get_config(Config::Displayname).await?, None);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
@@ -610,21 +613,24 @@ async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('displayname', 'Alice 2')",
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(alice.get_config(Config::Displayname).await?, None);
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::Displayname).await?,
|
||||
Some("Alice 2".to_string())
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
10
src/e2ee.rs
10
src/e2ee.rs
@@ -79,6 +79,16 @@ impl EncryptHelper {
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
/// Signs the passed-in `mail` using the private key from `context`.
|
||||
/// Returns the payload and the signature.
|
||||
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
let mut buffer = Vec::new();
|
||||
mail.clone().write_part(&mut buffer)?;
|
||||
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a private key exists for the configured user.
|
||||
|
||||
@@ -234,7 +234,8 @@ pub enum EventType {
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed, this parameter is set to `None`.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
|
||||
11
src/html.rs
11
src/html.rs
@@ -287,6 +287,7 @@ impl MsgId {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{self, Chat, forward_msgs, save_msgs};
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
@@ -554,7 +555,13 @@ test some special html-characters as < > and & but also " and &#x
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Alice receives a non-delta html-message
|
||||
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
|
||||
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
@@ -572,6 +579,10 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// receive the message on another device
|
||||
let alice = &tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.recv_msg(&msg).await;
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
assert_eq!(msg.get_from_id(), ContactId::SELF);
|
||||
|
||||
25
src/imap.rs
25
src/imap.rs
@@ -945,6 +945,29 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes all messages from IMAP folder.
|
||||
pub(crate) async fn delete_all_messages(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
|
||||
if self.select_with_uidvalidity(context, folder).await? {
|
||||
self.add_flag_finalized_with_set("1:*", "\\Deleted").await?;
|
||||
self.selected_folder_needs_expunge = true;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE transport_id=? AND folder=?",
|
||||
(transport_id, folder),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves batch of messages identified by their UID from the currently
|
||||
/// selected folder to the target folder.
|
||||
async fn move_message_batch(
|
||||
@@ -2001,7 +2024,7 @@ pub(crate) async fn prefetch_should_download(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let should_download = !blocked_contact || maybe_ndn;
|
||||
let should_download = (!blocked_contact) || maybe_ndn;
|
||||
Ok(should_download)
|
||||
}
|
||||
|
||||
|
||||
35
src/key.rs
35
src/key.rs
@@ -1,7 +1,7 @@
|
||||
//! Cryptographic key module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{self, Write as _};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
@@ -583,21 +583,6 @@ impl Fingerprint {
|
||||
pub fn hex(&self) -> String {
|
||||
hex::encode_upper(&self.0)
|
||||
}
|
||||
|
||||
/// Make a human-readable fingerprint.
|
||||
pub fn human_readable(&self) -> String {
|
||||
let mut f = String::new();
|
||||
// Split key into chunks of 4 with space and newline at 20 chars
|
||||
for (i, c) in self.hex().chars().enumerate() {
|
||||
if i > 0 && i % 20 == 0 {
|
||||
writeln!(&mut f).ok();
|
||||
} else if i > 0 && i % 4 == 0 {
|
||||
write!(&mut f, " ").ok();
|
||||
}
|
||||
write!(&mut f, "{c}").ok();
|
||||
}
|
||||
f
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::types::Fingerprint> for Fingerprint {
|
||||
@@ -614,6 +599,22 @@ impl fmt::Debug for Fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a human-readable fingerprint.
|
||||
impl fmt::Display for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Split key into chunks of 4 with space and newline at 20 chars
|
||||
for (i, c) in self.hex().chars().enumerate() {
|
||||
if i > 0 && i % 20 == 0 {
|
||||
writeln!(f)?;
|
||||
} else if i > 0 && i % 4 == 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
write!(f, "{c}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a human-readable or otherwise formatted fingerprint.
|
||||
impl std::str::FromStr for Fingerprint {
|
||||
type Err = anyhow::Error;
|
||||
@@ -889,7 +890,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
|
||||
]);
|
||||
assert_eq!(
|
||||
fp.human_readable(),
|
||||
fp.to_string(),
|
||||
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
mod authres;
|
||||
pub mod color;
|
||||
pub mod html;
|
||||
pub mod net;
|
||||
|
||||
119
src/location.rs
119
src/location.rs
@@ -264,11 +264,15 @@ impl Kml {
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> Result<()> {
|
||||
pub async fn send_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(seconds >= 0);
|
||||
ensure!(!chat_id.is_special());
|
||||
let now = time();
|
||||
let is_sending_locations_before = is_sending_to_chat(context, chat_id).await?;
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -301,49 +305,35 @@ pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether any chat is sending locations.
|
||||
pub async fn is_sending(context: &Context) -> Result<bool> {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?",
|
||||
(time(),),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether `chat_id` is sending locations.
|
||||
pub async fn is_sending_to_chat(context: &Context, chat_id: ChatId) -> Result<bool> {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?",
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a list of chats in which location streaming is enabled.
|
||||
async fn get_chats_with_location_streaming(context: &Context) -> Result<Vec<ChatId>> {
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?",
|
||||
(time(),),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stop sending locations in all chats.
|
||||
pub async fn stop_sending(context: &Context) -> Result<()> {
|
||||
for chat_id in get_chats_with_location_streaming(context).await? {
|
||||
send_to_chat(context, chat_id, 0).await?;
|
||||
}
|
||||
Ok(())
|
||||
/// Returns whether `chat_id` or any chat is sending locations.
|
||||
///
|
||||
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
|
||||
/// is sending locations.
|
||||
pub async fn is_sending_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
) -> Result<bool> {
|
||||
let exists = match chat_id {
|
||||
Some(chat_id) => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
(time(),),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Sets current location of the user device.
|
||||
@@ -469,6 +459,13 @@ fn is_marker(txt: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes all locations from the database.
|
||||
pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
context.sql.execute("DELETE FROM locations;", ()).await?;
|
||||
context.emit_location_changed(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes expired locations.
|
||||
///
|
||||
/// Only path locations are deleted.
|
||||
@@ -498,7 +495,7 @@ pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
|
||||
///
|
||||
/// This function is used when a message is deleted
|
||||
/// that has a corresponding `location_id`.
|
||||
pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
|
||||
pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -510,7 +507,7 @@ pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()
|
||||
}
|
||||
|
||||
/// Deletes POI locations that don't have corresponding message anymore.
|
||||
pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
|
||||
pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<()> {
|
||||
context.sql.execute("
|
||||
DELETE FROM locations
|
||||
WHERE independent=1 AND id NOT IN
|
||||
@@ -719,9 +716,9 @@ pub(crate) async fn save(
|
||||
|
||||
pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
|
||||
loop {
|
||||
let next_event = match maybe_send(context).await {
|
||||
let next_event = match maybe_send_locations(context).await {
|
||||
Err(err) => {
|
||||
warn!(context, "location::maybe_send failed: {:#}", err);
|
||||
warn!(context, "maybe_send_locations failed: {:#}", err);
|
||||
Some(60) // Retry one minute later.
|
||||
}
|
||||
Ok(next_event) => next_event,
|
||||
@@ -759,7 +756,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
|
||||
/// Returns number of seconds until the next time location streaming for some chat ends
|
||||
/// automatically.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
async fn maybe_send(context: &Context) -> Result<Option<u64>> {
|
||||
async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
let mut next_event: Option<u64> = None;
|
||||
|
||||
let now = time();
|
||||
@@ -871,7 +868,7 @@ mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[test]
|
||||
@@ -1054,7 +1051,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
send_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
|
||||
@@ -1103,13 +1100,10 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
.await?;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
// Bob needs the chat accepted so that "normal" messages from Alice trigger `IncomingMsg`.
|
||||
// Location-only messages still must trigger `MsgsChanged`.
|
||||
bob.create_chat(alice).await;
|
||||
|
||||
// Alice enables location streaming.
|
||||
// Bob receives a message saying that Alice enabled location streaming.
|
||||
send_to_chat(alice, alice_chat.id, 60).await?;
|
||||
send_locations_to_chat(alice, alice_chat.id, 60).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Alice gets new location from GPS.
|
||||
@@ -1119,19 +1113,8 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
// 10 seconds later location sending stream manages to send location.
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
delete_expired(alice, time()).await?;
|
||||
maybe_send(alice).await?;
|
||||
bob.evtracker.clear_events();
|
||||
maybe_send_locations(alice).await?;
|
||||
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
|
||||
bob.evtracker
|
||||
.get_matching_ex(
|
||||
bob,
|
||||
ExpectedEvents {
|
||||
expected: |e| matches!(e, EventType::MsgsChanged { .. }),
|
||||
unexpected: |e| matches!(e, EventType::IncomingMsg { .. }),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
|
||||
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ pub struct EnteredImapLoginParam {
|
||||
/// Folder to watch.
|
||||
///
|
||||
/// If empty, user has not entered anything and it shuold expand to "INBOX" later.
|
||||
#[serde(default)]
|
||||
pub folder: String,
|
||||
|
||||
/// Socket security.
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::location;
|
||||
use crate::location::delete_poi_location;
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -529,7 +529,7 @@ impl Message {
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON c.id=m.chat_id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE m.id=? AND chat_id!=3 -- DC_CHAT_ID_TRASH
|
||||
WHERE m.id=? AND chat_id!=3
|
||||
LIMIT 1",
|
||||
(id,),
|
||||
|row| {
|
||||
@@ -739,7 +739,7 @@ impl Message {
|
||||
/// at a position different from the self-location.
|
||||
/// You should not call this function
|
||||
/// if you want to bind the current self-location to a message;
|
||||
/// this is done by [`location::set()`] and [`location::send_to_chat()`].
|
||||
/// this is done by [`location::set()`] and [`send_locations_to_chat()`].
|
||||
///
|
||||
/// Typically results in the event [`LocationChanged`] with
|
||||
/// `contact_id` set to [`ContactId::SELF`].
|
||||
@@ -748,7 +748,7 @@ impl Message {
|
||||
/// `longitude` is the East-west position of the location.
|
||||
///
|
||||
/// [`location::set()`]: crate::location::set
|
||||
/// [`location::send_to_chat()`]: crate::location::send_to_chat
|
||||
/// [`send_locations_to_chat()`]: crate::location::send_locations_to_chat
|
||||
/// [`LocationChanged`]: crate::events::EventType::LocationChanged
|
||||
pub fn set_location(&mut self, latitude: f64, longitude: f64) {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
@@ -795,6 +795,12 @@ impl Message {
|
||||
self.viewtype
|
||||
}
|
||||
|
||||
/// Forces the message to **keep** [Viewtype::Sticker]
|
||||
/// e.g the message will not be converted to a [Viewtype::Image].
|
||||
pub fn force_sticker(&mut self) {
|
||||
self.param.set_int(Param::ForceSticker, 1);
|
||||
}
|
||||
|
||||
/// Returns the state of the message.
|
||||
pub fn get_state(&self) -> MessageState {
|
||||
self.state
|
||||
@@ -1649,7 +1655,7 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
|
||||
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
|
||||
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
|
||||
if msg.location_id > 0 {
|
||||
location::delete_poi(context, msg.location_id).await?;
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg.id
|
||||
@@ -2117,6 +2123,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
|
||||
/// Count messages older than the given number of `seconds`.
|
||||
///
|
||||
/// Returns the number of messages that are older than the given number of seconds.
|
||||
/// This includes e-mails downloaded due to the `show_emails` option.
|
||||
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub async fn estimate_deletion_cnt(
|
||||
@@ -2315,6 +2322,8 @@ pub enum Viewtype {
|
||||
Gif = 21,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
///
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
|
||||
@@ -1227,18 +1227,53 @@ impl MimeFactory {
|
||||
message.header(header, value)
|
||||
});
|
||||
let message = MimePart::new("multipart/mixed", vec![message]);
|
||||
let message = protected_headers
|
||||
let mut message = protected_headers
|
||||
.iter()
|
||||
.fold(message, |message, (header, value)| {
|
||||
message.header(*header, value.clone())
|
||||
});
|
||||
|
||||
// Deduplicate unprotected headers that also are in the protected headers:
|
||||
let protected: HashSet<&str> =
|
||||
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
|
||||
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
|
||||
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
|
||||
// Deduplicate unprotected headers that also are in the protected headers:
|
||||
let protected: HashSet<&str> =
|
||||
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
|
||||
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
|
||||
|
||||
message
|
||||
message
|
||||
} else {
|
||||
for (h, v) in &mut message.headers {
|
||||
if h == "Content-Type"
|
||||
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
|
||||
{
|
||||
let mut ct_new = ct.clone();
|
||||
ct_new = ct_new.attribute("protected-headers", "v1");
|
||||
if use_std_header_protection {
|
||||
ct_new = ct_new.attribute("hp", "clear");
|
||||
}
|
||||
*ct = ct_new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let signature = encrypt_helper.sign(context, &message).await?;
|
||||
MimePart::new(
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"; protected",
|
||||
vec![
|
||||
message,
|
||||
MimePart::new(
|
||||
"application/pgp-signature; name=\"signature.asc\"",
|
||||
signature,
|
||||
)
|
||||
.header(
|
||||
"Content-Description",
|
||||
mail_builder::headers::raw::Raw::<'static>::new(
|
||||
"OpenPGP digital signature",
|
||||
),
|
||||
)
|
||||
.attachment("signature"),
|
||||
],
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let MimeFactory {
|
||||
@@ -1794,7 +1829,7 @@ impl MimeFactory {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_to_chat(context, msg.chat_id).await?
|
||||
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
|
||||
&& let Some(part) = self.get_location_kml_part(context).await?
|
||||
{
|
||||
parts.push(part);
|
||||
@@ -2157,6 +2192,10 @@ fn group_headers_by_confidentiality(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the header to the protected headers
|
||||
// in case of signed-only message.
|
||||
// If the message is not signed, this value will not be used.
|
||||
protected_headers.push(header.clone());
|
||||
unprotected_headers.push(header.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,11 @@ async fn msg_to_subject_str_inner(
|
||||
|
||||
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
|
||||
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
receive_imf(context, imf_raw, false).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
@@ -601,6 +606,70 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_unencrypted_signed() {
|
||||
// create chat with bob, set selfavatar
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::SignUnencrypted, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
|
||||
|
||||
let file = t.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// send message to bob: that should get multipart/signed.
|
||||
// `Subject:` is protected by copying it.
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("multipart/signed").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 0);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
|
||||
let body = payload.next().unwrap();
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
}
|
||||
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
|
||||
@@ -14,8 +14,9 @@ use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrpars
|
||||
use mime::Mime;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactId, import_public_key};
|
||||
@@ -274,7 +275,7 @@ impl MimeMessage {
|
||||
let timestamp_rcvd = smeared_time(context);
|
||||
let mut timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
let mut hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut headers_removed = HashSet::<String>::new();
|
||||
@@ -304,9 +305,37 @@ impl MimeMessage {
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
let (part, mimetype) =
|
||||
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
|
||||
if let Some(part) = mail.subparts.first() {
|
||||
// We don't remove "subject" from `headers` because currently just signed
|
||||
// messages are shown as unencrypted anyway.
|
||||
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
part,
|
||||
);
|
||||
(part, part.ctype.mimetype.parse::<Mime>()?)
|
||||
} else {
|
||||
// Not a valid signed message, handle it as plaintext.
|
||||
(&mail, mimetype)
|
||||
}
|
||||
} else {
|
||||
// Currently we do not sign unencrypted messages by default.
|
||||
(&mail, mimetype)
|
||||
};
|
||||
if mimetype.type_() == mime::MULTIPART
|
||||
&& mimetype.subtype().as_str() == "mixed"
|
||||
&& let Some(part) = mail.subparts.first()
|
||||
&& let Some(part) = part.subparts.first()
|
||||
{
|
||||
for field in &part.headers {
|
||||
let key = field.get_key().to_lowercase();
|
||||
@@ -330,13 +359,18 @@ impl MimeMessage {
|
||||
);
|
||||
}
|
||||
|
||||
// Remove headers that are allowed _only_ in the encrypted+signed part
|
||||
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
|
||||
// them in signed-only emails, but has no value currently.
|
||||
let encrypted = false;
|
||||
Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
|
||||
|
||||
let mut from = from.context("No from in message")?;
|
||||
|
||||
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
|
||||
|
||||
let mut gossiped_keys = Default::default();
|
||||
hop_info += "\n\n";
|
||||
hop_info += &dkim_results.to_string();
|
||||
|
||||
let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
|
||||
|
||||
@@ -2098,7 +2132,7 @@ async fn parse_gossip_headers(
|
||||
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let header = match Aheader::from_str(value) {
|
||||
let header = match value.parse::<Aheader>() {
|
||||
Ok(header) => header,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
|
||||
@@ -2188,6 +2222,9 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
/// Returns whether the outer header value must be ignored if the message contains a signed (and
|
||||
/// optionally encrypted) part. This is independent from the modern Header Protection defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html>.
|
||||
///
|
||||
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
|
||||
/// signed-only messages. Such messages are shown as unencrypted anyway.
|
||||
fn is_protected(key: &str) -> bool {
|
||||
key.starts_with("chat-")
|
||||
|| matches!(
|
||||
@@ -2545,10 +2582,6 @@ async fn handle_ndn(
|
||||
|
||||
for msg_id in msg_ids {
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.chat_id).await?;
|
||||
if chat.typ == constants::Chattype::OutBroadcast {
|
||||
continue;
|
||||
}
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
chat,
|
||||
chatlist::Chatlist,
|
||||
constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
contact::Contact,
|
||||
key,
|
||||
message::{MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
@@ -2042,24 +2041,32 @@ async fn test_multiple_autocrypt_hdrs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests receiving a simple signed-unencrypted message
|
||||
/// that was generated by an old version of Core that supported sending such messages.
|
||||
/// Tests that timestamp of signed but not encrypted message is protected.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_signed_only() -> Result<()> {
|
||||
async fn test_protected_date() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let imf_raw = include_bytes!("../../test-data/message/unencrypted_signed_simple.eml");
|
||||
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
|
||||
assert_eq!(msg.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.get_text(), "Hello!");
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
assert_eq!(msg.get_timestamp(), 1615987853);
|
||||
alice.set_config(Config::SignUnencrypted, Some("1")).await?;
|
||||
|
||||
let alice_contact = Contact::get_by_id(bob, msg.from_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
let alice_msg_id = chat::send_text_msg(alice, alice_chat.id, "Hello!".to_string()).await?;
|
||||
let alice_msg = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
|
||||
let mut sent_msg = alice.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg.payload.replacen(
|
||||
"Date:",
|
||||
"Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)\r\nX-Not-Date:",
|
||||
1,
|
||||
);
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(alice_msg.get_text(), bob_msg.get_text());
|
||||
|
||||
// Timestamp that the sender has put into the message
|
||||
// should always be displayed as is on the receiver.
|
||||
assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +247,9 @@ pub enum Param {
|
||||
/// For Webxdc Message Instances: Chat to integrate the Webxdc for.
|
||||
WebxdcIntegrateFor = b'2',
|
||||
|
||||
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
||||
ForceSticker = b'X',
|
||||
|
||||
/// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete.
|
||||
DeleteRequestFor = b'M',
|
||||
|
||||
|
||||
49
src/pgp.rs
49
src/pgp.rs
@@ -6,15 +6,15 @@ use std::io::Cursor;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::composed::{
|
||||
Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType, MessageBuilder,
|
||||
SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
||||
SubkeyParamsBuilder, SubpacketConfig,
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{Signature, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
@@ -202,6 +202,47 @@ pub async fn pk_encrypt(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Produces a detached signature for `plain` text using `private_key_for_signing`.
|
||||
pub fn pk_calc_signature(
|
||||
plain: Vec<u8>,
|
||||
private_key_for_signing: &SignedSecretKey,
|
||||
) -> Result<String> {
|
||||
let rng = thread_rng();
|
||||
|
||||
let mut config = SignatureConfig::from_key(
|
||||
rng,
|
||||
&private_key_for_signing.primary_key,
|
||||
SignatureType::Binary,
|
||||
)?;
|
||||
|
||||
config.hashed_subpackets = vec![
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
private_key_for_signing.fingerprint(),
|
||||
))?,
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
pgp::types::Timestamp::now(),
|
||||
))?,
|
||||
];
|
||||
config.unhashed_subpackets = vec![];
|
||||
if private_key_for_signing.version() <= KeyVersion::V4 {
|
||||
config
|
||||
.unhashed_subpackets
|
||||
.push(Subpacket::regular(SubpacketData::IssuerKeyId(
|
||||
private_key_for_signing.legacy_key_id(),
|
||||
))?);
|
||||
}
|
||||
|
||||
let signature = config.sign(
|
||||
&private_key_for_signing.primary_key,
|
||||
&Password::empty(),
|
||||
plain.as_slice(),
|
||||
)?;
|
||||
|
||||
let sig = DetachedSignature::new(signature);
|
||||
|
||||
Ok(sig.to_armored_string(ArmorOptions::default())?)
|
||||
}
|
||||
|
||||
/// Returns fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures in `msg` and corresponding intended recipient fingerprints
|
||||
|
||||
@@ -645,7 +645,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
}
|
||||
} else {
|
||||
Ok(Qr::FprWithoutAddr {
|
||||
fingerprint: fingerprint.human_readable(),
|
||||
fingerprint: fingerprint.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
bob,
|
||||
&format!(
|
||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||
alice_contact.fingerprint().unwrap().hex()
|
||||
alice_contact.fingerprint().unwrap()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
98
src/quota.rs
98
src/quota.rs
@@ -6,17 +6,33 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session as ImapSession;
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{EventType, stock_str};
|
||||
|
||||
/// quota icon in connectivity is "yellow".
|
||||
/// warn about a nearly full mailbox after this usage percentage is reached.
|
||||
/// quota icon is "yellow".
|
||||
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
|
||||
|
||||
/// quota icon in connectivity is "red".
|
||||
/// warning again after this usage percentage is reached,
|
||||
/// quota icon is "red".
|
||||
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
|
||||
|
||||
/// if quota is below this value (again),
|
||||
/// QuotaExceeding is cleared.
|
||||
///
|
||||
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
|
||||
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
|
||||
///
|
||||
/// We do not repeat warnings on a daily base or so as some provider
|
||||
/// providers report bad values and we would then spam the user.
|
||||
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
|
||||
|
||||
/// Server quota information with an update timestamp.
|
||||
#[derive(Debug)]
|
||||
pub struct QuotaInfo {
|
||||
@@ -54,6 +70,37 @@ async fn get_unique_quota_roots_and_usage(
|
||||
Ok(unique_quota_roots)
|
||||
}
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &'t QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
for r in resources {
|
||||
let usage_percent = r.get_usage_percentage();
|
||||
match highest {
|
||||
None => {
|
||||
highest = Some((usage_percent, name, r));
|
||||
}
|
||||
Some((up, ..)) => {
|
||||
if up <= usage_percent {
|
||||
highest = Some((usage_percent, name, r));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
highest.context("no quota_resource found, this is unexpected")
|
||||
}
|
||||
|
||||
/// Checks if a quota warning is needed.
|
||||
pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> bool {
|
||||
(curr_percentage >= QUOTA_WARN_THRESHOLD_PERCENTAGE
|
||||
&& warned_at_percentage < QUOTA_WARN_THRESHOLD_PERCENTAGE)
|
||||
|| (curr_percentage >= QUOTA_ERROR_THRESHOLD_PERCENTAGE
|
||||
&& warned_at_percentage < QUOTA_ERROR_THRESHOLD_PERCENTAGE)
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||
/// called.
|
||||
@@ -87,6 +134,32 @@ impl Context {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self)))
|
||||
};
|
||||
|
||||
if let Ok(quota) = "a {
|
||||
match get_highest_usage(quota) {
|
||||
Ok((highest, _, _)) => {
|
||||
if needs_quota_warning(
|
||||
highest,
|
||||
self.get_config_int(Config::QuotaExceeding).await? as u64,
|
||||
) {
|
||||
self.set_config_internal(
|
||||
Config::QuotaExceeding,
|
||||
Some(&highest.to_string()),
|
||||
)
|
||||
.await?;
|
||||
let mut msg = Message::new_text(stock_str::quota_exceeding(self, highest));
|
||||
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
|
||||
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
|
||||
self.set_config_internal(Config::QuotaExceeding, None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => warn!(
|
||||
self,
|
||||
"Transport {transport_id}: Cannot get highest quota usage: {err:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
self.quota.write().await.insert(
|
||||
transport_id,
|
||||
QuotaInfo {
|
||||
@@ -106,10 +179,29 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_needs_quota_warning() -> Result<()> {
|
||||
assert!(!needs_quota_warning(0, 0));
|
||||
assert!(!needs_quota_warning(10, 0));
|
||||
assert!(!needs_quota_warning(70, 0));
|
||||
assert!(!needs_quota_warning(75, 0));
|
||||
assert!(!needs_quota_warning(79, 0));
|
||||
assert!(needs_quota_warning(80, 0));
|
||||
assert!(needs_quota_warning(81, 0));
|
||||
assert!(!needs_quota_warning(85, 80));
|
||||
assert!(!needs_quota_warning(85, 81));
|
||||
assert!(needs_quota_warning(95, 82));
|
||||
assert!(!needs_quota_warning(97, 95));
|
||||
assert!(!needs_quota_warning(97, 96));
|
||||
assert!(!needs_quota_warning(1000, 96));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::assertions_on_constants)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_thresholds() -> anyhow::Result<()> {
|
||||
assert!(0 < QUOTA_WARN_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
|
||||
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
|
||||
Ok(())
|
||||
|
||||
@@ -36,7 +36,10 @@ pub struct Reaction {
|
||||
reaction: String,
|
||||
}
|
||||
|
||||
impl Reaction {
|
||||
// We implement From<&str> instead of std::str::FromStr, because
|
||||
// FromStr requires error type and reaction parsing never returns an
|
||||
// error.
|
||||
impl From<&str> for Reaction {
|
||||
/// Convert a `&str` into a `Reaction`.
|
||||
/// Everything after the first whitespace is ignored.
|
||||
///
|
||||
@@ -48,7 +51,7 @@ impl Reaction {
|
||||
/// reactions is not different from other kinds of spam attacks
|
||||
/// such as sending large numbers of large messages, and should be
|
||||
/// dealt with the same way, e.g. by blocking the user.
|
||||
pub fn new(reaction: &str) -> Self {
|
||||
fn from(reaction: &str) -> Self {
|
||||
let reaction: &str = reaction
|
||||
.split_ascii_whitespace()
|
||||
.next()
|
||||
@@ -58,7 +61,9 @@ impl Reaction {
|
||||
reaction: reaction.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Reaction {
|
||||
/// Returns true if reaction contains no emoji.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.reaction.is_empty()
|
||||
@@ -207,7 +212,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
let reaction = Reaction::new(reaction);
|
||||
let reaction: Reaction = reaction.into();
|
||||
let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
|
||||
reaction_msg.set_reaction();
|
||||
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
|
||||
@@ -277,7 +282,7 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
let reaction: String = row.get(1)?;
|
||||
Ok((contact_id, Reaction::new(reaction.as_str())))
|
||||
Ok((contact_id, Reaction::from(reaction.as_str())))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -356,32 +361,32 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_reaction() {
|
||||
// Check that basic set of emojis from RFC 9078 is supported.
|
||||
assert_eq!(Reaction::new("👍").as_str(), "👍");
|
||||
assert_eq!(Reaction::new("👎").as_str(), "👎");
|
||||
assert_eq!(Reaction::new("😀").as_str(), "😀");
|
||||
assert_eq!(Reaction::new("☹").as_str(), "☹");
|
||||
assert_eq!(Reaction::new("😢").as_str(), "😢");
|
||||
assert_eq!(Reaction::from("👍").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👎").as_str(), "👎");
|
||||
assert_eq!(Reaction::from("😀").as_str(), "😀");
|
||||
assert_eq!(Reaction::from("☹").as_str(), "☹");
|
||||
assert_eq!(Reaction::from("😢").as_str(), "😢");
|
||||
|
||||
// Empty string can be used to remove all reactions.
|
||||
assert!(Reaction::new("").is_empty());
|
||||
assert!(Reaction::from("").is_empty());
|
||||
|
||||
// Short strings can be used as emojis, could be used to add
|
||||
// support for custom emojis via emoji shortcodes.
|
||||
assert_eq!(Reaction::new(":deltacat:").as_str(), ":deltacat:");
|
||||
assert_eq!(Reaction::from(":deltacat:").as_str(), ":deltacat:");
|
||||
|
||||
// Check that long strings are not valid emojis.
|
||||
assert!(
|
||||
Reaction::new(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
|
||||
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
|
||||
);
|
||||
|
||||
// Multiple reactions separated by spaces or tabs are not supported.
|
||||
assert_eq!(Reaction::new("👍 ❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::new("👍\t❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👍 ❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👍\t❤").as_str(), "👍");
|
||||
|
||||
assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍");
|
||||
|
||||
assert_eq!(Reaction::new("👍 👍").as_str(), "👍");
|
||||
assert_eq!(Reaction::from("👍 👍").as_str(), "👍");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -577,7 +582,7 @@ Content-Disposition: reaction\n\
|
||||
assert_eq!(chat_id, expected_chat_id);
|
||||
assert_eq!(msg_id, expected_msg_id);
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
assert_eq!(reaction, Reaction::new(expected_reaction));
|
||||
assert_eq!(reaction, Reaction::from(expected_reaction));
|
||||
}
|
||||
_ => panic!("Unexpected event {event:?}."),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use std::cmp;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::iter;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
@@ -12,13 +11,14 @@ use deltachat_contact_tools::{
|
||||
sanitize_single_line,
|
||||
};
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, ChatVisibility, is_contact_in_chat, save_broadcast_secret,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX};
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
@@ -525,15 +525,52 @@ pub(crate) async fn receive_imf_inner(
|
||||
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
|
||||
);
|
||||
|
||||
// These checks must be done before processing of SecureJoin and other special messages.
|
||||
// check, if the mail is already in our database.
|
||||
// make sure, this check is done eg. before securejoin-processing.
|
||||
let (replace_msg_id, replace_chat_id);
|
||||
if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
|
||||
// Post-Message just replaces the attachment and modifies Params, not the whole message.
|
||||
// This is done in the `handle_post_message` method.
|
||||
} else if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await? {
|
||||
info!(
|
||||
context,
|
||||
"Message {rfc724_mid} is already in some chat or deleted."
|
||||
);
|
||||
replace_msg_id = None;
|
||||
replace_chat_id = None;
|
||||
} else if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
// This code handles the download of old partial download stub messages
|
||||
// It will be removed after a transitioning period,
|
||||
// after we have released a few versions with pre-messages
|
||||
replace_msg_id = Some(old_msg_id);
|
||||
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
||||
.await?
|
||||
.filter(|msg| msg.download_state() != DownloadState::Done)
|
||||
{
|
||||
// The message was partially downloaded before.
|
||||
match mime_parser.pre_message {
|
||||
PreMessageMode::Post | PreMessageMode::None => {
|
||||
info!(context, "Message already partly in DB, replacing.");
|
||||
Some(msg.chat_id)
|
||||
}
|
||||
PreMessageMode::Pre { .. } => {
|
||||
info!(context, "Cannot replace pre-message with a pre-message");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The message was already fully downloaded
|
||||
// or cannot be loaded because it is deleted.
|
||||
None
|
||||
};
|
||||
} else {
|
||||
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
|
||||
None
|
||||
} else {
|
||||
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
|
||||
};
|
||||
replace_chat_id = None;
|
||||
}
|
||||
|
||||
if replace_chat_id.is_some() {
|
||||
// Need to update chat id in the db.
|
||||
} else if let Some(msg_id) = replace_msg_id {
|
||||
info!(context, "Message is already downloaded.");
|
||||
if mime_parser.incoming {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -552,7 +589,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let prevent_rename = should_prevent_rename(&mime_parser);
|
||||
|
||||
@@ -602,7 +639,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
mime_parser.get_header(HeaderDef::References),
|
||||
mime_parser.get_header(HeaderDef::InReplyTo),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.filter(|p| Some(p.id) != replace_msg_id);
|
||||
|
||||
let mut chat_assignment =
|
||||
decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
|
||||
@@ -682,8 +720,20 @@ pub(crate) async fn receive_imf_inner(
|
||||
MessengerMessage::No
|
||||
};
|
||||
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
|
||||
let allow_creation = if mime_parser.decryption_error.is_some() {
|
||||
false
|
||||
} else if is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
{
|
||||
// the message is a classic email in a classic profile
|
||||
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
|
||||
match show_emails {
|
||||
ShowEmails::Off | ShowEmails::AcceptedContacts => false,
|
||||
ShowEmails::All => true,
|
||||
}
|
||||
} else {
|
||||
!mime_parser.parts.iter().all(|part| part.is_reaction)
|
||||
};
|
||||
@@ -718,6 +768,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
rfc724_mid_orig,
|
||||
from_id,
|
||||
seen,
|
||||
replace_msg_id,
|
||||
prevent_rename,
|
||||
chat_id,
|
||||
chat_id_blocked,
|
||||
@@ -1012,6 +1063,11 @@ UPDATE msgs SET state=? WHERE
|
||||
.await?;
|
||||
} else if received_msg.hidden {
|
||||
// No need to emit an event about the changed message
|
||||
} else if let Some(replace_chat_id) = replace_chat_id {
|
||||
match replace_chat_id == chat_id {
|
||||
false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
|
||||
true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
|
||||
}
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh
|
||||
&& mime_parser.is_system_message != SystemMessage::CallAccepted
|
||||
@@ -1019,15 +1075,8 @@ UPDATE msgs SET state=? WHERE
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
|
||||
let skip_bot_notify = is_bot && is_pre_message;
|
||||
let is_empty = !is_pre_message
|
||||
&& mime_parser.parts.first().is_none_or(|p| {
|
||||
p.typ == Viewtype::Text && p.msg.is_empty() && p.param.get(Param::Quote).is_none()
|
||||
});
|
||||
let important = mime_parser.incoming
|
||||
&& !is_empty
|
||||
&& fresh
|
||||
&& !is_old_contact_request
|
||||
&& !skip_bot_notify;
|
||||
let important =
|
||||
mime_parser.incoming && fresh && !is_old_contact_request && !skip_bot_notify;
|
||||
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
chat_id.emit_msg_event(context, *msg_id, important);
|
||||
@@ -1205,6 +1254,20 @@ async fn decide_chat_assignment(
|
||||
}
|
||||
info!(context, "Outgoing undecryptable message (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& !mime_parser.has_chat_version()
|
||||
&& parent_message
|
||||
.as_ref()
|
||||
.is_none_or(|p| p.is_dc_message == MessengerMessage::No)
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
&& ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default()
|
||||
== ShowEmails::Off
|
||||
{
|
||||
info!(context, "Classical email not shown (TRASH).");
|
||||
// the message is a classic email in a classic profile
|
||||
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
|
||||
true
|
||||
} else if mime_parser
|
||||
.get_header(HeaderDef::XMozillaDraftInfo)
|
||||
.is_some()
|
||||
@@ -1745,6 +1808,7 @@ async fn add_parts(
|
||||
rfc724_mid: &str,
|
||||
from_id: ContactId,
|
||||
seen: bool,
|
||||
mut replace_msg_id: Option<MsgId>,
|
||||
prevent_rename: bool,
|
||||
mut chat_id: ChatId,
|
||||
mut chat_id_blocked: Blocked,
|
||||
@@ -1836,7 +1900,7 @@ async fn add_parts(
|
||||
// Extract ephemeral timer from the message
|
||||
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
|
||||
{
|
||||
match EphemeralTimer::from_str(value) {
|
||||
match value.parse::<EphemeralTimer>() {
|
||||
Ok(timer) => timer,
|
||||
Err(err) => {
|
||||
warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
|
||||
@@ -2114,7 +2178,7 @@ async fn add_parts(
|
||||
chat_id,
|
||||
from_id,
|
||||
sort_timestamp,
|
||||
Reaction::new(reaction_str.as_str()),
|
||||
Reaction::from(reaction_str.as_str()),
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?;
|
||||
@@ -2125,6 +2189,22 @@ async fn add_parts(
|
||||
param.set_int(Param::Cmd, is_system_message as i32);
|
||||
}
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
let placeholder = Message::load_from_db(context, replace_msg_id)
|
||||
.await
|
||||
.context("Failed to load placeholder message")?;
|
||||
for key in [
|
||||
Param::WebxdcSummary,
|
||||
Param::WebxdcSummaryTimestamp,
|
||||
Param::WebxdcDocument,
|
||||
Param::WebxdcDocumentTimestamp,
|
||||
] {
|
||||
if let Some(value) = placeholder.param.get(key) {
|
||||
param.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
@@ -2167,9 +2247,10 @@ async fn add_parts(
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"
|
||||
r#"
|
||||
INSERT INTO msgs
|
||||
(
|
||||
id,
|
||||
rfc724_mid, pre_rfc724_mid, chat_id,
|
||||
from_id, to_id, timestamp, timestamp_sent,
|
||||
timestamp_rcvd, type, state, msgrmsg,
|
||||
@@ -2179,29 +2260,34 @@ INSERT INTO msgs
|
||||
ephemeral_timestamp, download_state, hop_info
|
||||
)
|
||||
VALUES (
|
||||
?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, 1,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?
|
||||
)",
|
||||
)?;
|
||||
let params = params![
|
||||
if let PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid,
|
||||
..
|
||||
} = &mime_parser.pre_message
|
||||
{
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
|
||||
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
|
||||
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
|
||||
RETURNING id
|
||||
"#)?;
|
||||
let row_id: MsgId = stmt.query_row(params![
|
||||
replace_msg_id,
|
||||
if let PreMessageMode::Pre {post_msg_rfc724_mid, ..} = &mime_parser.pre_message {
|
||||
post_msg_rfc724_mid
|
||||
} else {
|
||||
} else { rfc724_mid_orig },
|
||||
if let PreMessageMode::Pre {..} = &mime_parser.pre_message {
|
||||
rfc724_mid_orig
|
||||
},
|
||||
if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
|
||||
rfc724_mid_orig
|
||||
} else {
|
||||
""
|
||||
},
|
||||
} else { "" },
|
||||
if trash { DC_CHAT_ID_TRASH } else { chat_id },
|
||||
if trash { ContactId::UNDEFINED } else { from_id },
|
||||
if trash { ContactId::UNDEFINED } else { to_id },
|
||||
@@ -2210,27 +2296,13 @@ INSERT INTO msgs
|
||||
if trash { 0 } else { mime_parser.timestamp_rcvd },
|
||||
if trash {
|
||||
Viewtype::Unknown
|
||||
} else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
Viewtype::Text
|
||||
} else {
|
||||
typ
|
||||
},
|
||||
if trash {
|
||||
MessageState::Undefined
|
||||
} else {
|
||||
state
|
||||
},
|
||||
if trash {
|
||||
MessengerMessage::No
|
||||
} else {
|
||||
is_dc_message
|
||||
},
|
||||
} else { typ },
|
||||
if trash { MessageState::Undefined } else { state },
|
||||
if trash { MessengerMessage::No } else { is_dc_message },
|
||||
if trash || hidden { "" } else { msg },
|
||||
if trash || hidden {
|
||||
None
|
||||
} else {
|
||||
normalize_text(msg)
|
||||
},
|
||||
if trash || hidden { None } else { normalize_text(msg) },
|
||||
if trash || hidden { "" } else { &subject },
|
||||
if trash {
|
||||
"".to_string()
|
||||
@@ -2247,28 +2319,33 @@ INSERT INTO msgs
|
||||
if trash { "" } else { mime_in_reply_to },
|
||||
if trash { "" } else { mime_references },
|
||||
!trash && save_mime_modified,
|
||||
if trash {
|
||||
""
|
||||
} else {
|
||||
part.error.as_deref().unwrap_or_default()
|
||||
},
|
||||
if trash { "" } else { part.error.as_deref().unwrap_or_default() },
|
||||
if trash { 0 } else { ephemeral_timer.to_u32() },
|
||||
if trash { 0 } else { ephemeral_timestamp },
|
||||
if trash {
|
||||
DownloadState::Done
|
||||
} else if mime_parser.decryption_error.is_some() {
|
||||
DownloadState::Undecipherable
|
||||
} else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
|
||||
} else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
|
||||
DownloadState::Available
|
||||
} else {
|
||||
DownloadState::Done
|
||||
},
|
||||
if trash { "" } else { &mime_parser.hop_info },
|
||||
];
|
||||
let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
|
||||
],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
}
|
||||
)?;
|
||||
Ok(row_id)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// We only replace placeholder with a first part,
|
||||
// afterwards insert additional parts.
|
||||
replace_msg_id = None;
|
||||
|
||||
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
|
||||
created_db_entries.push(row_id);
|
||||
}
|
||||
@@ -2293,6 +2370,14 @@ INSERT INTO msgs
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
// Trash the "replace" placeholder with a message that has no parts. If it has the original
|
||||
// "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
|
||||
// fully downloaded message later, the server-side deletion is issued.
|
||||
let on_server = rfc724_mid == rfc724_mid_orig;
|
||||
replace_msg_id.trash(context, on_server).await?;
|
||||
}
|
||||
|
||||
let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
Some(addr) => context.is_self_addr(addr).await?,
|
||||
None => true,
|
||||
@@ -2396,7 +2481,6 @@ async fn handle_edit_delete(
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.from_id == from_id {
|
||||
@@ -2411,8 +2495,6 @@ async fn handle_edit_delete(
|
||||
}
|
||||
} 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).await?;
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
@@ -78,8 +78,9 @@ static GRP_MAIL: &[u8] =
|
||||
hello\n";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_is_shown() {
|
||||
async fn test_adhoc_group_show_chats_only() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("0")).await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
@@ -94,12 +95,66 @@ async fn test_adhoc_group_is_shown() {
|
||||
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(
|
||||
Config::ShowEmails,
|
||||
Some(&ShowEmails::AcceptedContacts.to_string()),
|
||||
)
|
||||
.await?;
|
||||
tcm.send_recv(alice, bob, "hi").await;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: bob@example.net\n\
|
||||
To: alice@example.org, claire@example.com\n\
|
||||
Message-ID: <3333@example.net>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
Contact::create(&t, "Bob", "bob@example.com").await.unwrap();
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
|
||||
// (and existent chat is required)
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
|
||||
|
||||
// accept Bob by accepting a delta-message from Bob
|
||||
receive_imf(&t, MSGRMSG, false).await.unwrap();
|
||||
@@ -135,6 +190,7 @@ async fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_show_all() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 2);
|
||||
receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat
|
||||
@@ -760,6 +816,10 @@ async fn test_concat_multiple_ndns() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
let received_msg = receive_imf(context, imf_raw, false)
|
||||
.await
|
||||
.expect("receive_imf failure")
|
||||
|
||||
@@ -250,6 +250,16 @@ impl SchedulerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_all_relay_storage(&self) -> Result<()> {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.clear_all_relay_storage();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IO is not started");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
@@ -348,6 +358,7 @@ async fn inbox_loop(
|
||||
let ImapConnectionHandlers {
|
||||
mut connection,
|
||||
stop_token,
|
||||
clear_storage_request_receiver,
|
||||
} = inbox_handlers;
|
||||
|
||||
let transport_id = connection.transport_id();
|
||||
@@ -386,7 +397,14 @@ async fn inbox_loop(
|
||||
}
|
||||
};
|
||||
|
||||
match inbox_fetch_idle(&ctx, &mut connection, session).await {
|
||||
match inbox_fetch_idle(
|
||||
&ctx,
|
||||
&mut connection,
|
||||
session,
|
||||
&clear_storage_request_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => warn!(
|
||||
ctx,
|
||||
"Transport {transport_id}: Failed inbox fetch_idle: {err:#}."
|
||||
@@ -407,11 +425,29 @@ async fn inbox_loop(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
|
||||
async fn inbox_fetch_idle(
|
||||
ctx: &Context,
|
||||
imap: &mut Imap,
|
||||
mut session: Session,
|
||||
clear_storage_request_receiver: &Receiver<()>,
|
||||
) -> Result<Session> {
|
||||
let transport_id = session.transport_id();
|
||||
|
||||
// Clear IMAP storage on request.
|
||||
//
|
||||
// Only doing this for chatmail relays to avoid
|
||||
// accidentally deleting all emails in a shared mailbox.
|
||||
let should_clear_imap_storage =
|
||||
clear_storage_request_receiver.try_recv().is_ok() && session.is_chatmail();
|
||||
if should_clear_imap_storage {
|
||||
info!(ctx, "Transport {transport_id}: Clearing IMAP storage.");
|
||||
session.delete_all_messages(ctx, &imap.folder).await?;
|
||||
}
|
||||
|
||||
// Update quota no more than once a minute.
|
||||
if ctx.quota_needs_update(session.transport_id(), 60).await
|
||||
//
|
||||
// Always update if we just cleared IMAP storage.
|
||||
if (ctx.quota_needs_update(session.transport_id(), 60).await || should_clear_imap_storage)
|
||||
&& let Err(err) = ctx.update_recent_quota(&mut session, &imap.folder).await
|
||||
{
|
||||
warn!(
|
||||
@@ -737,6 +773,12 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_all_relay_storage(&self) {
|
||||
for b in &self.inboxes {
|
||||
b.conn_state.clear_relay_storage();
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
}
|
||||
@@ -870,6 +912,13 @@ struct SmtpConnectionHandlers {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImapConnectionState {
|
||||
state: ConnectionState,
|
||||
|
||||
/// Channel to request clearing the folder.
|
||||
///
|
||||
/// IMAP loop receiving this should clear the folder
|
||||
/// on the next iteration if IMAP server is a chatmail relay
|
||||
/// and otherwise ignore the request.
|
||||
clear_storage_request_sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl ImapConnectionState {
|
||||
@@ -881,11 +930,13 @@ impl ImapConnectionState {
|
||||
) -> Result<(Self, ImapConnectionHandlers)> {
|
||||
let stop_token = CancellationToken::new();
|
||||
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
|
||||
let (clear_storage_request_sender, clear_storage_request_receiver) = channel::bounded(1);
|
||||
|
||||
let handlers = ImapConnectionHandlers {
|
||||
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
|
||||
.await?,
|
||||
stop_token: stop_token.clone(),
|
||||
clear_storage_request_receiver,
|
||||
};
|
||||
|
||||
let state = ConnectionState {
|
||||
@@ -894,7 +945,10 @@ impl ImapConnectionState {
|
||||
connectivity: handlers.connection.connectivity.clone(),
|
||||
};
|
||||
|
||||
let conn = ImapConnectionState { state };
|
||||
let conn = ImapConnectionState {
|
||||
state,
|
||||
clear_storage_request_sender,
|
||||
};
|
||||
|
||||
Ok((conn, handlers))
|
||||
}
|
||||
@@ -908,10 +962,19 @@ impl ImapConnectionState {
|
||||
fn stop(&self) {
|
||||
self.state.stop();
|
||||
}
|
||||
|
||||
/// Requests clearing relay storage and interrupts the inbox.
|
||||
fn clear_relay_storage(&self) {
|
||||
self.clear_storage_request_sender.try_send(()).ok();
|
||||
self.state.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ImapConnectionHandlers {
|
||||
connection: Imap,
|
||||
stop_token: CancellationToken,
|
||||
|
||||
/// Channel receiver to get requests to clear IMAP storage.
|
||||
pub(crate) clear_storage_request_receiver: Receiver<()>,
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
let auth = create_id();
|
||||
token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
|
||||
|
||||
let fingerprint = self_fingerprint(context).await?;
|
||||
let fingerprint = get_self_fingerprint(context).await?.hex();
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
|
||||
@@ -861,8 +861,7 @@ fn encrypted_and_signed(
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.human_readable()
|
||||
"Message does not match expected fingerprint {expected_fingerprint}.",
|
||||
);
|
||||
false
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
@@ -902,7 +902,7 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
|
||||
// Delete POI locations
|
||||
// which don't have corresponding message.
|
||||
location::delete_orphaned_poi(context)
|
||||
delete_orphaned_poi_locations(context)
|
||||
.await
|
||||
.context("Failed to delete orphaned POI locations")
|
||||
.log_err(context)
|
||||
|
||||
@@ -12,6 +12,7 @@ use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::warn;
|
||||
@@ -974,7 +975,8 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
|
||||
// keep this default and use DC_SHOW_EMAILS_NO
|
||||
// only for new installations
|
||||
if exists_before_update {
|
||||
sql.set_raw_config_int("show_emails", 2).await?;
|
||||
sql.set_raw_config_int("show_emails", ShowEmails::All as i32)
|
||||
.await?;
|
||||
}
|
||||
sql.set_db_version(50).await?;
|
||||
}
|
||||
@@ -1455,7 +1457,8 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
}
|
||||
if dbversion < 98 {
|
||||
if exists_before_update && sql.get_raw_config_int("show_emails").await?.is_none() {
|
||||
sql.set_raw_config_int("show_emails", 0).await?;
|
||||
sql.set_raw_config_int("show_emails", ShowEmails::Off as i32)
|
||||
.await?;
|
||||
}
|
||||
sql.set_db_version(98).await?;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,15 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Forwarded"))]
|
||||
Forwarded = 97,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
|
||||
You may not be able to receive message when the storage is 100%% used.\n\n\
|
||||
👉 Please check if you can delete old data in the provider's webinterface \
|
||||
and consider to enable \"Settings / Delete Old Messages\". \
|
||||
You can check your current storage usage anytime at \"Settings / Connectivity\"."
|
||||
))]
|
||||
QuotaExceedingMsgBody = 98,
|
||||
|
||||
#[strum(props(fallback = "Multi Device Synchronization"))]
|
||||
SyncMsgSubject = 101,
|
||||
|
||||
@@ -1091,6 +1100,13 @@ pub(crate) fn forwarded(context: &Context) -> String {
|
||||
translated(context, StockMessage::Forwarded)
|
||||
}
|
||||
|
||||
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
|
||||
pub(crate) fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||
translated(context, StockMessage::QuotaExceedingMsgBody)
|
||||
.replace1(&format!("{highest_usage}"))
|
||||
.replace("%%", "%")
|
||||
}
|
||||
|
||||
/// Stock string: `Incoming Messages`.
|
||||
pub(crate) fn incoming_messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::IncomingMessages)
|
||||
|
||||
@@ -102,6 +102,16 @@ async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = quota_exceeding(&t, 81);
|
||||
assert!(str.contains("81% "));
|
||||
assert!(str.contains("100% "));
|
||||
assert!(!str.contains("%%"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -793,7 +793,8 @@ ORDER BY id"
|
||||
let chat_msgs = chat::get_chat_msgs(self, received.chat_id).await.unwrap();
|
||||
assert!(
|
||||
chat_msgs.contains(&ChatItem::Message { msg_id: msg.id }),
|
||||
"received message is not shown in chat, maybe it's hidden"
|
||||
"received message is not shown in chat, maybe it's hidden (you may have \
|
||||
to call set_config(Config::ShowEmails, Some(\"2\")).await)"
|
||||
);
|
||||
|
||||
msg
|
||||
@@ -1104,6 +1105,7 @@ ORDER BY id"
|
||||
self,
|
||||
chat_id,
|
||||
MessageListOptions {
|
||||
info_only: false,
|
||||
add_daymarker: false,
|
||||
},
|
||||
)
|
||||
@@ -1431,12 +1433,6 @@ pub fn fiona_keypair() -> SignedSecretKey {
|
||||
#[derive(Debug)]
|
||||
pub struct EventTracker(EventEmitter);
|
||||
|
||||
/// See [`super::EventTracker::get_matching_ex`].
|
||||
pub struct ExpectedEvents<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool> {
|
||||
pub expected: E,
|
||||
pub unexpected: U,
|
||||
}
|
||||
|
||||
impl Deref for EventTracker {
|
||||
type Target = EventEmitter;
|
||||
|
||||
@@ -1473,39 +1469,21 @@ impl EventTracker {
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes all emitted events returning the first matching one if any.
|
||||
/// Consumes emitted events returning the first matching one if any.
|
||||
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
event_matcher: F,
|
||||
) -> Option<EventType> {
|
||||
self.get_matching_ex(
|
||||
ctx,
|
||||
ExpectedEvents {
|
||||
expected: event_matcher,
|
||||
unexpected: |_| false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Consumes all emitted events returning the first matching one if any. Panics on unexpected
|
||||
/// events.
|
||||
pub async fn get_matching_ex<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
args: ExpectedEvents<E, U>,
|
||||
) -> Option<EventType> {
|
||||
ctx.emit_event(EventType::Test);
|
||||
let mut found_event = None;
|
||||
loop {
|
||||
let event = self.recv().await.unwrap();
|
||||
assert!(!(args.unexpected)(&event.typ));
|
||||
if let EventType::Test = event.typ {
|
||||
return found_event;
|
||||
}
|
||||
if (args.expected)(&event.typ) {
|
||||
found_event.get_or_insert(event.typ);
|
||||
if event_matcher(&event.typ) {
|
||||
found_event = Some(event.typ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ use crate::download::DownloadState;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
// The code for replacing partial download stubs is already removed, so check that nothing happens
|
||||
// if after that a full message is passed to receive_imf. Users should ask the sender to send the
|
||||
// message again.
|
||||
// The code for downloading stub messages stays
|
||||
// during the transition perios to pre-messages
|
||||
// so people can still download their files shortly after they updated.
|
||||
// After there are a few release with pre-message rolled out,
|
||||
// we will remove the ability to download stub messages and replace the following test
|
||||
// so it checks that it doesn't crash or that the messages are replaced by sth.
|
||||
// like "download failed/expired, please ask sender to send it again"
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_stub_message() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -49,9 +53,9 @@ async fn test_download_stub_message() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(msg.get_text().contains("[97.66 KiB message]"));
|
||||
assert_eq!(msg.get_text(), "100k text...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, sen
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{DcKey, load_self_public_key};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -152,7 +152,11 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
bob.sql
|
||||
.execute(
|
||||
"DELETE FROM public_keys WHERE fingerprint=?",
|
||||
(&self_fingerprint(alice).await.unwrap(),),
|
||||
(&load_self_public_key(alice)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint()
|
||||
.hex(),),
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
|
||||
@@ -44,7 +44,9 @@ async fn test_parse_receive_headers_integration() {
|
||||
Message-ID: 2dfdbde7@example.org
|
||||
|
||||
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000
|
||||
|
||||
DKIM Results: Passed=true";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml");
|
||||
@@ -54,7 +56,9 @@ Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
|
||||
|
||||
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
|
||||
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
|
||||
DKIM Results: Passed=true";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -422,6 +422,13 @@ async fn check_addrs(
|
||||
a.get_published_self_addrs().await.unwrap(),
|
||||
published_self_addrs.clone(),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[
|
||||
addresses.secondary_published,
|
||||
addresses.secondary_unpublished,
|
||||
]),
|
||||
);
|
||||
assert_eq(
|
||||
a.get_published_secondary_self_addrs().await.unwrap(),
|
||||
concat(&[addresses.secondary_published]),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas106.aol.mail.ne1.yahoo.com;
|
||||
dkim=pass header.i=@buzon.uy header.s=2019;
|
||||
spf=pass smtp.mailfrom=buzon.uy;
|
||||
dmarc=pass(p=REJECT) header.from=buzon.uy;
|
||||
From: <alice@buzon.uy>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
|
||||
dkim=unknown;
|
||||
spf=none smtp.mailfrom=delta.blinzeln.de;
|
||||
dmarc=unknown header.from=delta.blinzeln.de;
|
||||
From: <alice@delta.blinzeln.de>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas210.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@disroot.org header.s=mail;
|
||||
spf=pass smtp.mailfrom=disroot.org;
|
||||
dmarc=pass(p=QUARANTINE) header.from=disroot.org;
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: atlas105.aol.mail.ne1.yahoo.com;
|
||||
dkim=pass header.i=@fastmail.com header.s=fm2;
|
||||
dkim=pass header.i=@messagingengine.com header.s=fm2;
|
||||
spf=pass smtp.mailfrom=fastmail.com;
|
||||
dmarc=pass(p=NONE,sp=NONE) header.from=fastmail.com;
|
||||
From: <alice@fastmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-baseline-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@gmail.com header.s=20210112;
|
||||
spf=pass smtp.mailfrom=gmail.com;
|
||||
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=gmail.com;
|
||||
From: <alice@gmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,8 @@
|
||||
Authentication-Results: atlas112.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@hotmail.com header.s=selector1;
|
||||
spf=pass smtp.mailfrom=hotmail.com;
|
||||
dmarc=pass(p=NONE) header.from=hotmail.com;
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@hotmail.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas101.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@icloud.com header.s=1a1hai;
|
||||
spf=pass smtp.mailfrom=icloud.com;
|
||||
dmarc=pass(p=QUARANTINE,sp=QUARANTINE) header.from=icloud.com;
|
||||
From: <alice@icloud.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@ik.me header.s=20200325;
|
||||
spf=pass smtp.mailfrom=ik.me;
|
||||
dmarc=pass(p=REJECT) header.from=ik.me;
|
||||
From: <alice@ik.me>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas104.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@mail.ru header.s=mail4;
|
||||
spf=pass smtp.mailfrom=mail.ru;
|
||||
dmarc=pass(p=REJECT) header.from=mail.ru;
|
||||
From: <alice@mail.ru>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas211.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@mailo.com header.s=mailo;
|
||||
spf=pass smtp.mailfrom=mailo.com;
|
||||
dmarc=pass(p=NONE) header.from=mailo.com;
|
||||
From: <alice@mailo.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,8 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@outlook.com header.s=selector1;
|
||||
spf=pass smtp.mailfrom=outlook.com;
|
||||
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=outlook.com;
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@outlook.com>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
|
||||
dkim=pass header.i=@posteo.de header.s=2017;
|
||||
spf=pass smtp.mailfrom=posteo.de;
|
||||
dmarc=pass(p=NONE) header.from=posteo.de;
|
||||
From: <alice@posteo.de>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas114.aol.mail.bf1.yahoo.com;
|
||||
dkim=pass header.i=@yandex.ru header.s=mail;
|
||||
spf=pass smtp.mailfrom=yandex.ru;
|
||||
dmarc=pass(p=NONE) header.from=yandex.ru;
|
||||
From: <alice@yandex.ru>
|
||||
To: <alice@aol.com>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
|
||||
dkim=unknown;
|
||||
spf=none smtp.mailfrom=delta.blinzeln.de;
|
||||
dmarc=unknown header.from=delta.blinzeln.de;
|
||||
From: forged-authres-added@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=aol.com header.i=@aol.com header.b="sjmqxpKe";
|
||||
dkim-atps=neutral
|
||||
From: <alice@aol.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,3 @@
|
||||
From: <alice@delta.blinzeln.de>
|
||||
To: <alice@buzon.uy>
|
||||
Authentication-Results: secure-mailgate.com; auth=pass smtp.auth=91.203.111.88@webbox222.server-home.org
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="L9SmOHOj";
|
||||
dkim-atps=neutral
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=fastmail.com header.i=@fastmail.com header.b="kLB05is1";
|
||||
dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="B8mfR89g";
|
||||
dkim-atps=neutral
|
||||
From: <alice@fastmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="Ngf1X5eN";
|
||||
dkim-atps=neutral
|
||||
From: <alice@gmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=hotmail.com header.i=@hotmail.com header.b="dEHn9Szj";
|
||||
dkim-atps=neutral
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@hotmail.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=icloud.com header.i=@icloud.com header.b="rAXD4xVN";
|
||||
dkim-atps=neutral
|
||||
From: <alice@icloud.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; secure) header.d=ik.me header.i=@ik.me header.b="EWWQpVZX";
|
||||
dkim-atps=neutral
|
||||
From: <alice@ik.me>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=mail.de header.i=@mail.de header.b="18cRkjHf";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mail.de>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=mail.ru header.i=@mail.ru header.b="uXBGAnnn";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mail.ru>
|
||||
To: <alice@buzon.uy>
|
||||
Authentication-Results: smtpng1.m.smailru.net; auth=pass smtp.auth=alice@mail.ru smtp.mailfrom=alice@mail.ru
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=mailo.com header.i=@mailo.com header.b="awx9eOw9";
|
||||
dkim-atps=neutral
|
||||
From: <alice@mailo.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,7 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=outlook.com header.i=@outlook.com header.b="Uq5LH/n/";
|
||||
dkim-atps=neutral
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
From: <alice@outlook.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; secure) header.d=posteo.de header.i=@posteo.de header.b="AyOucyBM";
|
||||
dkim-atps=neutral
|
||||
From: <alice@posteo.de>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; secure) header.d=riseup.net header.i=@riseup.net header.b="eQhRD1BM";
|
||||
dkim-atps=neutral
|
||||
From: <alice@riseup.net>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,5 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=yahoo.com header.i=@yahoo.com header.b="a1T2PpDI";
|
||||
dkim-atps=neutral
|
||||
From: <alice@yahoo.com>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,6 @@
|
||||
Authentication-Results: mail.buzon.uy;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.b="oGBtMMzR";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: iva4-143b1447cf50.qloud-c.yandex.net; dkim=pass header.i=@yandex.ru
|
||||
From: <alice@yandex.ru>
|
||||
To: <alice@buzon.uy>
|
||||
@@ -0,0 +1,3 @@
|
||||
From: forged-authres-added@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@aol.com>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@buzon.uy>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
@@ -0,0 +1,2 @@
|
||||
From: <alice@disroot.org>
|
||||
To: <alice@delta.blinzeln.de>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user