Compare commits

..

1 Commits

Author SHA1 Message Date
iequidoo
08458500ac fix: create_send_msg_jobs: Don't pass message ID to smtp
Follow-up to 970222f. When resending to new broadcast members, we shouldn't update the message db
entry and also share the message id with existing SMTP jobs:
- Otherwise the behavior would depend on timings which isn't good.
- If the message sending failed, resending it to a new member shalln't make it `OutDelivered`.
2026-04-23 13:15:30 -03:00
380 changed files with 4418 additions and 1042 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &lt; &gt; and &amp; but also &quot; 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 &lt; &gt; and &amp; but also &quot; 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) = &quota {
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(())

View File

@@ -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:?}."),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
From: <alice@aol.com>
To: <alice@delta.blinzeln.de>

View File

@@ -0,0 +1,2 @@
From: <alice@buzon.uy>
To: <alice@delta.blinzeln.de>

View File

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