Compare commits

..
Author SHA1 Message Date
link2xt 715030bc42 feat: take key flags and expiration into account when selecting the key
Test key was generated with GnuPG 2.4.9 from Arch Linux.
The script that was used to generate the key:
```
export GNUPGHOME="$PWD/gnupghome"
rm -fr "$GNUPGHOME"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"

# --faked-system-time example is from the gpg man page
GPG="gpg --batch --quiet --faked-system-time 20070924T154812"

USERID='Alice <alice@example.org>'

# Generate the primary key.
# "default" means certification+signing primary key
# "never" to make the key not expire, otherwise gpg generates expiring key by default.
$GPG --passphrase '' --quick-generate-key "$USERID" ed25519 default never

# Can as well use USERID everywhere to refer to the key, but better use the fingerprint.
# Output of --with-colons is described in /usr/share/doc/gnupg/DETAILS
FINGERPRINT="$($GPG --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10 }')"

# Authentication-only RSA key, should not be used for encryption.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" rsa auth never

# Expired (considering the fake date) subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr 1d

# Signing subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" ed25519 sign never

# Usable encryption subkey.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr never

# Expiring subkey that is not expired in the beginning of 2008.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr 1y

# Another encryption subkey that should not be used because the first one is preferred.
$GPG --passphrase '' --quick-add-key "$FINGERPRINT" cv25519 encr never

$GPG --armor --export "$FINGERPRINT" > alice.tpk.asc
```
2026-09-25 18:39:44 +00:00
link2xt bc662ca91c refactor: remove Context argument from secret_key_to_public_key() 2026-09-25 13:21:19 +00:00
43 changed files with 892 additions and 851 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.99.0
RUST_VERSION: 1.98.1
# Minimum Supported Rust Version
MSRV: 1.89.0
@@ -132,7 +132,7 @@ jobs:
cache-bin: false
- name: Install nextest
uses: taiki-e/install-action@9983c65e42da123ff25d1f78505eb6de315aa172
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc
with:
tool: nextest
+7 -7
View File
@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- run: nix fmt flake.nix nix/ -- --check
build:
@@ -85,7 +85,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -106,5 +106,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- run: nix build .#${{ matrix.installable }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary
+2 -2
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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- 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@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
Generated
+74 -74
View File
@@ -194,9 +194,9 @@ dependencies = [
[[package]]
name = "astral-tokio-tar"
version = "0.7.0"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f2e989b33246fe9240d39accf4dd9a01e0b6c1f3ce9dd095e0a47fa02505523"
checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463"
dependencies = [
"futures-core",
"libc",
@@ -205,7 +205,6 @@ dependencies = [
"rustix 1.1.4",
"tokio",
"tokio-stream",
"zerocopy 0.8.59",
]
[[package]]
@@ -311,7 +310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79"
dependencies = [
"native-tls",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"url",
]
@@ -328,7 +327,7 @@ dependencies = [
"log",
"nom 8.0.0",
"pin-project",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
]
@@ -364,7 +363,7 @@ dependencies = [
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tokio-util",
]
@@ -460,7 +459,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"thiserror 2.0.21",
"thiserror 2.0.20",
]
[[package]]
@@ -1387,6 +1386,7 @@ dependencies = [
"regex",
"rusqlite",
"sanitize-filename",
"sdp",
"serde",
"serde_json",
"sha-1",
@@ -1399,7 +1399,7 @@ dependencies = [
"tempfile",
"testdir",
"textwrap",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tokio-io-timeout",
"tokio-rustls",
@@ -1503,7 +1503,7 @@ name = "deltachat_derive"
version = "2.0.0"
dependencies = [
"quote",
"syn 3.0.6",
"syn 3.0.4",
]
[[package]]
@@ -1518,7 +1518,7 @@ dependencies = [
"num-traits",
"rand 0.9.4",
"serde_json",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"yerpc",
]
@@ -1677,9 +1677,9 @@ dependencies = [
[[package]]
name = "dirs"
version = "7.0.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
@@ -2222,7 +2222,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.4",
]
[[package]]
@@ -2465,7 +2465,7 @@ dependencies = [
"once_cell",
"rand 0.9.4",
"ring",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tinyvec",
"tokio",
"tracing",
@@ -2488,7 +2488,7 @@ dependencies = [
"rand 0.9.4",
"resolv-conf",
"smallvec",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tracing",
]
@@ -2654,9 +2654,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.11.1"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
@@ -2694,16 +2694,15 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.21"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body",
"httparse",
"hyper",
"libc",
"pin-project-lite",
@@ -3046,7 +3045,7 @@ dependencies = [
"strum 0.26.2",
"stun-rs",
"surge-ping",
"thiserror 2.0.21",
"thiserror 2.0.20",
"time",
"tokio",
"tokio-stream",
@@ -3071,7 +3070,7 @@ dependencies = [
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
"thiserror 2.0.21",
"thiserror 2.0.20",
"url",
]
@@ -3113,7 +3112,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serde-error",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tokio-util",
"tracing",
@@ -3158,7 +3157,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
@@ -3178,7 +3177,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
@@ -3233,7 +3232,7 @@ dependencies = [
"sha1",
"strum 0.26.2",
"stun-rs",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3451,9 +3450,9 @@ checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
[[package]]
name = "mailparse"
version = "0.17.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8469955699e572455f8e1a40689e07ab309911d659632f65062598192254c091"
checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
dependencies = [
"charset",
"data-encoding",
@@ -3718,7 +3717,7 @@ dependencies = [
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.21",
"thiserror 2.0.20",
]
[[package]]
@@ -4207,7 +4206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.21",
"thiserror 2.0.20",
"ucd-trie",
]
@@ -4383,7 +4382,7 @@ dependencies = [
"serde",
"sha1_smol",
"simple-dns",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tracing",
"url",
@@ -4596,7 +4595,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy 0.7.35",
"zerocopy",
]
[[package]]
@@ -4781,7 +4780,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.9",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tracing",
]
@@ -4802,7 +4801,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
@@ -5041,7 +5040,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.21",
"thiserror 2.0.20",
]
[[package]]
@@ -5445,6 +5444,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdp"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22c3b0257608d7de4de4c4ea650ccc2e6e3e45e3cd80039fcdee768bcb449253"
dependencies = [
"rand 0.9.4",
"substring",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "sec1"
version = "0.7.3"
@@ -5550,7 +5561,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.4",
]
[[package]]
@@ -5706,7 +5717,7 @@ dependencies = [
"shadowsocks-crypto",
"socket2 0.5.9",
"spin 0.10.1",
"thiserror 2.0.21",
"thiserror 2.0.20",
"tokio",
"tokio-tfo",
"trait-variant",
@@ -5813,14 +5824,14 @@ dependencies = [
"sha3",
"signature",
"typenum",
"zerocopy 0.7.35",
"zerocopy",
]
[[package]]
name = "smallvec"
version = "1.16.1"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "smawk"
@@ -6003,6 +6014,15 @@ dependencies = [
"rand 0.9.4",
]
[[package]]
name = "substring"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
dependencies = [
"autocfg",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -6049,9 +6069,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.6"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
@@ -6195,11 +6215,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.21"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl 2.0.21",
"thiserror-impl 2.0.20",
]
[[package]]
@@ -6215,13 +6235,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.21"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
"syn 3.0.4",
]
[[package]]
@@ -6341,9 +6361,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
version = "0.26.5"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
@@ -6743,9 +6763,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.26.1"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -7409,7 +7429,7 @@ dependencies = [
"futures",
"log",
"serde",
"thiserror 2.0.21",
"thiserror 2.0.20",
"windows 0.59.0",
"windows-core 0.59.0",
]
@@ -7583,16 +7603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
"zerocopy-derive 0.8.59",
"zerocopy-derive",
]
[[package]]
@@ -7606,17 +7617,6 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
+4 -3
View File
@@ -36,6 +36,7 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { workspace = true }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
@@ -86,6 +87,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
sanitize-filename = { workspace = true }
sdp = "0.17.1"
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
@@ -100,7 +102,7 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false, features = ["tls12", "brotli"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.7.0", default-features = false }
astral-tokio-tar = { version = "0.6.3", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
@@ -113,7 +115,6 @@ webpki-roots = "0.26.8"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.8.1", features = ["async_tokio"] }
deltachat-time = { path = "./deltachat-time" }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -188,7 +189,7 @@ futures = "0.3.32"
futures-lite = "2.6.1"
libc = "0.2"
log = "0.4"
mailparse = "0.17.0"
mailparse = "0.16.1"
nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.9"
+3 -3
View File
@@ -21,9 +21,9 @@ fn main() {
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = option_env!("PREFIX").unwrap_or_else(|| "/usr/local"),
libdir = option_env!("LIBDIR").unwrap_or_else(|| "/usr/local/lib"),
includedir = option_env!("INCLUDEDIR").unwrap_or_else(|| "/usr/local/include"),
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
+1 -1
View File
@@ -8,7 +8,7 @@ repository = "https://github.com/chatmail/core"
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
dirs = "7"
dirs = "6"
log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
+1 -49
View File
@@ -20,7 +20,7 @@ use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::qr::*;
use deltachat::qr_code_generator::{create_qr_svg, get_securejoin_qr_svg};
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
@@ -346,57 +346,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
============================================="
),
},
"connect" => {
context.start_io().await;
}
"disconnect" => {
context.stop_io().await;
}
"fetch" => {
context.background_fetch().await?;
}
"configure" => {
context.configure().await?;
}
"has-backup" => {
has_backup(&context, blobdir).await?;
}
"clear" => {
println!("\n\n\n");
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
context.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = deltachat::securejoin::get_securejoin_qr(&context, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
}
}
"getqrsvg" => {
context.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&context, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {err}");
}
}
}
"joinqr" => {
context.start_io().await;
if !arg0.is_empty() {
deltachat::securejoin::join_securejoin(&context, arg1).await?;
}
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(
+89 -8
View File
@@ -13,6 +13,8 @@ use anyhow::{Error, bail};
use deltachat::EventType;
use deltachat::chat::ChatId;
use deltachat::context::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
@@ -23,6 +25,7 @@ use rustyline::validate::Validator;
use rustyline::{
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing_subscriber::EnvFilter;
@@ -349,16 +352,23 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str())?;
let line = line.trim();
if matches!(line, "exit" | "quit") {
println!("Exiting ...");
let should_continue = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
eprintln!("Error: {err:#}");
true
}
}
});
if !should_continue {
break;
}
if let Err(err) =
Handle::current().block_on(cmdline(ctx.clone(), line, &mut selected_chat))
{
eprintln!("Error: {err:#}");
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
@@ -382,6 +392,77 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(())
}
#[derive(Debug)]
enum ExitResult {
Continue,
Exit,
}
async fn handle_cmd(
line: &str,
ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
match arg0 {
"connect" => {
ctx.start_io().await;
}
"disconnect" => {
ctx.stop_io().await;
}
"fetch" => {
ctx.background_fetch().await?;
}
"configure" => {
ctx.configure().await?;
}
"clear" => {
println!("\n\n\n");
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
qr2term::print_qr(qr.as_str())?;
}
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {err}");
}
}
}
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),
_ => cmdline(ctx.clone(), line, selected_chat).await?,
}
Ok(ExitResult::Continue)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
@@ -149,10 +149,7 @@ class Rpc:
def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
# JSON-RPC error may happen if RPC server process has crashed already.
# We still want to shutdown all threads in this case.
with contextlib.suppress(JsonRpcError):
self.stop_io_for_all_accounts()
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
@@ -351,11 +351,12 @@ def test_qr_works_after_removing_primary_transport(acf, log) -> None:
log.section("Alice creates a QR code")
chat_qr = alice.get_qr_code()
chat_qr_unquoted = urllib.parse.unquote(chat_qr)
assert f"&a={third_addr}" in chat_qr_unquoted
assert f"&r={second_addr},{first_addr}" in chat_qr_unquoted
assert f"&a={first_addr}" in chat_qr_unquoted
assert f"&r={third_addr},{second_addr}" in chat_qr_unquoted
log.section("Alice removes the transport named by the a= parameter")
alice.delete_transport(third_addr)
log.section("Alice removes first and second transport")
alice.set_config("configured_addr", third_addr)
alice.delete_transport(first_addr)
alice.delete_transport(second_addr)
log.section("Bob scans the QR code, which still works")
@@ -1,5 +1,4 @@
import logging
import time
import pytest
@@ -91,30 +90,6 @@ def test_qr_securejoin(acf):
fiona.wait_for_securejoin_joiner_success()
def test_qr_securejoin_request_deleted_on_all_transports(acf):
alice, bob = acf.get_online_accounts(2)
alice.add_transport_from_qr(acf.get_account_qr())
alice.bring_online()
alice_chat = alice.create_group("Group")
qr_code = alice_chat.get_qr_code()
alice2 = alice.clone()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
alice_chat.remove_contact(bob)
# Stop and start io in order to wait until the remove message is sent out.
alice.stop_io()
alice.bring_online()
# Membership timestamps have a resolution of one second.
time.sleep(1)
# By now, the securejoin message sent by Bob must be deleted on Alice's relays.
# Otherwise, alice2 would execute securejoin again and re-add Bob.
alice2.bring_online()
alice2_chat = alice2.get_chat_by_id(alice_chat.id)
assert alice2.create_contact(bob) not in alice2_chat.get_contacts()
@pytest.mark.parametrize("all_devices_online", [True, False])
def test_qr_securejoin_broadcast(acf, all_devices_online):
alice, bob, fiona = acf.get_online_accounts(3)
@@ -35,19 +35,6 @@ def test_sleep(rpc) -> None:
assert sleep_5_future in pending
def test_shutdown(rpc) -> None:
"""Test RPC client shutdown if RPC server process is terminated.
This is a regression test, at the time of adding it RPC client
did not terminate all threads and python process did not exit.
Shutting down all threads is tested implicitly by pytest-timeout.
"""
rpc.process.kill()
rpc.process.wait()
with pytest.raises(JsonRpcError):
rpc.get_system_info()
def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
-2
View File
@@ -113,8 +113,6 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "zerocopy-derive", version = "0.7.35" },
{ name = "zerocopy", version = "0.7.35" },
]
-2
View File
@@ -34,8 +34,6 @@ const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
"chat.nuvon.app",
"chat.tinydispatch.org",
"chat.vim.wtf",
"chatmail.au",
"chatmail.cc",
"chatmail.uk",
"chtml.ca",
"deltachat.me",
+30 -8
View File
@@ -164,9 +164,9 @@ impl<'a> BlobObject<'a> {
/// you want to create a [BlobObject] for a filename read from the
/// database.
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
let name = match name.strip_prefix("$BLOBDIR/") {
Some(name) => name,
None => name,
let name = match name.starts_with("$BLOBDIR/") {
true => name.splitn(2, '/').last().unwrap(),
false => name,
};
if !BlobObject::is_acceptible_blob_name(name) {
return Err(format_err!("not an acceptable blob name: {name}"));
@@ -289,7 +289,6 @@ impl<'a> BlobObject<'a> {
name: Option<String>,
viewtype: &mut Viewtype,
) -> Result<String> {
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
let (max_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
@@ -327,7 +326,6 @@ impl<'a> BlobObject<'a> {
max_bytes: usize,
is_avatar: bool,
) -> Result<String> {
debug_assert!(matches!(viewtype, Viewtype::File | Viewtype::Image));
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = is_avatar;
let mut no_exif = false;
@@ -342,9 +340,17 @@ impl<'a> BlobObject<'a> {
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file))
.with_guessed_format()
.context("Failed to guess image format due to I/O error")?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
let imgreader = match imgreader {
Ok(ir) => ir,
_ => {
file.rewind()?;
ImageReader::with_format(
std::io::BufReader::new(&file),
ImageFormat::from_path(self.to_abs_path())?,
)
}
};
let fmt = imgreader.format().context("Unknown format")?;
if *vt == Viewtype::File {
*vt = Viewtype::Image;
@@ -357,6 +363,22 @@ impl<'a> BlobObject<'a> {
.unwrap_or(Orientation::NoTransforms);
let mut encoded = Vec::new();
if *vt == Viewtype::Sticker {
let x_max = img.width().saturating_sub(1);
let y_max = img.height().saturating_sub(1);
if !img.in_bounds(x_max, y_max)
|| !(img.get_pixel(0, 0).0[3] == 0
|| img.get_pixel(x_max, 0).0[3] == 0
|| img.get_pixel(0, y_max).0[3] == 0
|| img.get_pixel(x_max, y_max).0[3] == 0)
{
*vt = Viewtype::Image;
} else {
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
// from UIs shouldn't contain sensitive Exif info.
return Ok(name);
}
}
img.apply_orientation(orientation);
// max_wh is the maximum image width and height, i.e. the resolution-limit,
+8 -13
View File
@@ -2924,7 +2924,9 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
);
}
msg.subject.clone_from(&side_effects.subject);
if let Some(ref side_effects) = side_effects {
msg.subject.clone_from(&side_effects.subject);
}
if is_encrypted {
msg.param.set_int(Param::GuaranteeE2ee, 1);
} else {
@@ -2966,13 +2968,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
now,
msg.id,
&queued_pre_msg,
Some(&pre_side_effects),
pre_side_effects.as_ref(),
)
.context("Failed to enqueue pre-message")?;
row_ids.push(row_id)
}
row_ids.push(
enqueue_mail(transaction, now, msg.id, &queued_msg, Some(&side_effects))
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
.context("Failed to enqueue message")?,
);
Ok(row_ids)
@@ -4793,14 +4795,12 @@ pub(crate) async fn get_chat_id_by_grpid(
///
/// Optional `label` can be provided to ensure that message is added only once.
/// If `important` is true, a notification will be sent.
/// `timestamp_sent` is the time shown on the message; it does not affect ordering.
#[expect(clippy::arithmetic_side_effects)]
pub async fn add_device_msg_with_importance(
context: &Context,
label: Option<&str>,
msg: Option<&mut Message>,
important: bool,
timestamp_sent: i64,
) -> Result<MsgId> {
ensure!(
label.is_some() || msg.is_some(),
@@ -4820,10 +4820,11 @@ pub async fn add_device_msg_with_importance(
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid();
let timestamp_sent = time();
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
msg.timestamp_sort = time();
msg.timestamp_sort = timestamp_sent;
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
&& msg.timestamp_sort <= last_msg_time
{
@@ -4891,7 +4892,7 @@ pub async fn add_device_msg(
label: Option<&str>,
msg: Option<&mut Message>,
) -> Result<MsgId> {
add_device_msg_with_importance(context, label, msg, false, time()).await
add_device_msg_with_importance(context, label, msg, false).await
}
/// Returns true if device message with a given label was ever added to the device chat.
@@ -4919,12 +4920,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
context
.set_config_internal(Config::BackupTransferMsgId, None)
.await?;
context
.set_config_internal(Config::BackupTransferTimestamp, None)
.await?;
// Insert labels for welcome messages to avoid them being re-added on reconfiguration.
context
-11
View File
@@ -336,17 +336,6 @@ pub enum Config {
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
/// ID of the device message added after backup transfer.
///
/// If the message does not exist on the first housekeeping
/// after `READD_BACKUP_TRANSFER_MSG_DELAY`, it is re-added.
BackupTransferMsgId,
/// Timestamp of the device message added after backup transfer.
///
/// Needed to check against `READD_BACKUP_TRANSFER_MSG_DELAY`.
BackupTransferTimestamp,
/// Timestamp of the last time accumulated broadcast channel reactions were sent
LastReactionsBroadcast,
-2
View File
@@ -294,8 +294,6 @@ async fn test_get_info_completeness() {
// too sensitive or summarized in another item.
let skip_from_get_info = vec![
"addr",
"backup_transfer_msg_id",
"backup_transfer_timestamp",
"displayname",
"imap_certificate_checks",
"mail_server",
-1
View File
@@ -30,7 +30,6 @@ use crate::tools::{
mod transfer;
use ::pgp::types::KeyDetails;
pub(crate) use transfer::maybe_readd_backup_transfer_msg;
pub use transfer::{BackupProvider, get_backup};
// Name of the database file in the backup.
+7 -97
View File
@@ -40,68 +40,21 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::EventType;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::key;
use crate::log::warn;
use crate::message::{Message, MsgId};
use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{TempPathGuard, create_id, time};
use crate::tools::{TempPathGuard, create_id};
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
/// ALPN protocol identifier for the backup transfer protocol.
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
/// Minimum time after a backup transfer before we check
/// whether the "second device added" device message still exists.
///
/// The check is done in the first housekeeping after this delay;
/// if the message was deleted until then, it is re-added with the original timestamp.
/// After the check, the user can delete the message as usual without it being re-added.
///
/// This makes it easier to spot unwanted "add second device" actions.
/// It is clear that this does not catch all eventualities, it is best-effort.
/// First line of defense is device locking and asking for secret explicitly before adding a second device.
const READD_BACKUP_TRANSFER_MSG_DELAY: i64 = 60 * 60;
/// Checks `Config::BackupTransferMsgId`
/// and re-adds the backup transfer device message if it does not exist and some time passed.
pub(crate) async fn maybe_readd_backup_transfer_msg(context: &Context) -> Result<()> {
let Some(msg_id) = context
.get_config_parsed::<u32>(Config::BackupTransferMsgId)
.await?
else {
return Ok(());
};
let timestamp = context
.get_config_i64(Config::BackupTransferTimestamp)
.await?;
if time() < timestamp.saturating_add(READD_BACKUP_TRANSFER_MSG_DELAY) {
return Ok(());
}
context
.set_config_internal(Config::BackupTransferMsgId, None)
.await?;
context
.set_config_internal(Config::BackupTransferTimestamp, None)
.await?;
if Message::load_from_db_optional(context, MsgId::new(msg_id))
.await?
.is_none()
{
let mut msg = Message::new_text(backup_transfer_msg_body(context));
add_device_msg_with_importance(context, None, Some(&mut msg), false, timestamp).await?;
}
Ok(())
}
/// Provide or send a backup of this device.
///
/// This creates a backup of the current device and starts a service which offers another
@@ -259,23 +212,8 @@ impl BackupProvider {
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let timestamp = time();
let mut msg = Message::new_text(backup_transfer_msg_body(&context));
let msg_id =
add_device_msg_with_importance(&context, None, Some(&mut msg), false, timestamp)
.await?;
context
.set_config_internal(
Config::BackupTransferTimestamp,
Some(&timestamp.to_string()),
)
.await?;
context
.set_config_internal(
Config::BackupTransferMsgId,
Some(&msg_id.to_u32().to_string()),
)
.await?;
add_device_msg(&context, None, Some(&mut msg)).await?;
Ok(())
}
@@ -455,17 +393,14 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
mod tests {
use std::time::Duration;
use crate::chat::{ChatId, ChatItem, get_chat_msgs, send_msg};
use crate::contact::ContactId;
use crate::message::{Viewtype, delete_msgs};
use crate::sql::housekeeping;
use crate::chat::{ChatItem, get_chat_msgs, send_msg};
use crate::message::Viewtype;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_receive() -> Result<()> {
async fn test_send_receive() {
let mut tcm = TestContextManager::new();
// Create first device.
@@ -537,31 +472,6 @@ mod tests {
.get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
.await;
}
// When deleting the backup transfer message on the sending device,
// it is re-added once on housekeeping,
// but not within the first hour after the transfer.
let device_chat_id = ChatId::get_for_contact(&ctx0, ContactId::DEVICE).await?;
let original_msg = ctx0.get_last_msg_in(device_chat_id).await;
assert_eq!(original_msg.text, backup_transfer_msg_body(&ctx0));
delete_msgs(&ctx0, &[original_msg.id]).await?;
housekeeping(&ctx0).await?;
assert!(get_chat_msgs(&ctx0, device_chat_id).await?.is_empty()); // re-adding not done in first hour
SystemTime::shift(Duration::from_secs(60 * 60));
housekeeping(&ctx0).await?;
let readded_msg = ctx0.get_last_msg_in(device_chat_id).await;
assert_ne!(readded_msg.id, original_msg.id);
assert_eq!(readded_msg.get_text(), original_msg.get_text());
assert_eq!(readded_msg.get_timestamp(), original_msg.get_timestamp());
assert!(readded_msg.get_sort_timestamp() > original_msg.get_sort_timestamp());
delete_msgs(&ctx0, &[readded_msg.id]).await?;
housekeeping(&ctx0).await?;
let device_msgs = get_chat_msgs(&ctx0, device_chat_id).await?;
assert!(device_msgs.is_empty()); // re-adding is done only once
Ok(())
}
/// Tests that trying to accidentally overwrite a profile
+2 -6
View File
@@ -98,8 +98,7 @@ pub trait DcKey: Serialize + Deserializable + Clone {
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf)
.expect("Writing to Vec<u8> cannot fail");
self.to_writer(&mut buf).unwrap();
buf
}
@@ -125,14 +124,11 @@ pub trait DcKey: Serialize + Deserializable + Clone {
/// Converts secret key to public key.
pub(crate) fn secret_key_to_public_key(
context: &Context,
mut signed_secret_key: SignedSecretKey,
timestamp: u32,
addr: &str,
relay_addrs: &str,
) -> Result<SignedPublicKey> {
info!(context, "Converting secret key to public key.");
// Make sure timestamp of created signatures
// is not in the past compared to the primary key timestamp.
let timestamp = std::cmp::max(
@@ -305,7 +301,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
let addr = context.get_primary_self_addr().await?;
let all_addrs = context.get_self_addrs().await?.join(",");
let signed_public_key =
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
secret_key_to_public_key(signed_secret_key, timestamp, &addr, &all_addrs)?;
*lock = Some(signed_public_key.clone());
Ok(Some(signed_public_key))
+3 -8
View File
@@ -2017,19 +2017,14 @@ pub(crate) async fn set_msg_failed(
/// Inserts a tombstone into `msgs` table
/// to prevent downloading the same message in the future.
/// With `on_server`, copies seen on IMAP later are deleted on the server.
///
/// Returns tombstone database row ID.
pub(crate) async fn insert_tombstone(
context: &Context,
rfc724_mid: &str,
on_server: bool,
) -> Result<MsgId> {
pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
let row_id = context
.sql
.insert(
"INSERT INTO msgs(rfc724_mid, chat_id, deleted) VALUES (?,?,?)",
(rfc724_mid, ChatId::TRASH, on_server),
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, ChatId::TRASH),
)
.await?;
let msg_id = MsgId::new(u32::try_from(row_id)?);
+413 -339
View File
@@ -26,6 +26,7 @@ use crate::download::PostMsgMetadata;
use crate::ensure_and_debug_assert;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::headerdef::HeaderDef;
use crate::key;
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey, load_self_public_key, self_fingerprint};
use crate::location;
use crate::log::warn;
@@ -56,6 +57,19 @@ use crate::webxdc::StatusUpdateSerial;
/// and divide by 4/3 to account for base64 encoding.
pub const RECOMMENDED_FILE_SIZE: u64 = (30 - 1) * 1024 * 1024 / 4 * 3;
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub enum Loaded {
Message {
chat: Chat,
msg: Message,
},
Mdn {
rfc724_mid: String,
additional_msg_ids: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum PreMessageMode {
/// adds the Chat-Is-Post-Message header in unprotected part
@@ -171,10 +185,7 @@ pub struct MimeFactory {
member_timestamps: Vec<i64>,
timestamp: i64,
chat: Chat,
msg: Message,
loaded: Loaded,
in_reply_to: String,
/// List of Message-IDs for `References` header.
@@ -442,8 +453,8 @@ pub(crate) async fn render_queued_mail_with_context(
context: &Context,
) -> Result<RenderedEmail> {
let from_addr = context.get_primary_self_addr().await?;
let public_key = crate::key::load_self_public_key(context).await?;
let secret_key = crate::key::load_self_secret_key(context).await?;
let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let rendered_mail = render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?;
Ok(rendered_mail)
@@ -500,6 +511,11 @@ impl MimeFactory {
let mut member_fingerprints = Vec::new();
let mut member_timestamps = Vec::new();
let mut recipient_ids = HashSet::new();
let req_mdn = !chat.is_self_talk()
&& !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.should_request_mdns().await?;
let self_fingerprint = self_fingerprint(context).await?;
let encryption = if chat.is_self_talk() {
@@ -780,12 +796,6 @@ impl MimeFactory {
}
};
let req_mdn = encryption.is_encrypted()
&& !chat.is_self_talk()
&& !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.should_request_mdns().await?;
let (in_reply_to, references) = context
.sql
.query_row(
@@ -839,8 +849,7 @@ impl MimeFactory {
member_fingerprints,
member_timestamps,
timestamp: msg.timestamp_sort,
chat,
msg,
loaded: Loaded::Message { msg, chat },
in_reply_to,
references,
req_mdn,
@@ -851,6 +860,62 @@ impl MimeFactory {
Ok(factory)
}
pub async fn from_mdn(
context: &Context,
from_id: ContactId,
rfc724_mid: String,
additional_msg_ids: Vec<String>,
) -> Result<MimeFactory> {
let contact = Contact::get_by_id(context, from_id).await?;
let from_addr = context.get_primary_self_addr().await?;
let timestamp = time();
let addr = contact.get_addr().to_string();
let mut recipients = vec![addr.clone()];
let encryption = if from_id == ContactId::SELF {
Encryption::Asymmetric {
encryption_pubkeys: Vec::new(),
}
} else if contact.is_key_contact() {
let encryption_pubkeys = if let Some(key) = contact.public_key(context).await? {
recipients = relay_addrs(&key, &addr);
vec![(addr.clone(), key)]
} else {
Vec::new()
};
Encryption::Asymmetric { encryption_pubkeys }
} else {
Encryption::No
};
let res = MimeFactory {
from_addr,
from_displayname: "".to_string(),
sender_displayname: None,
selfstatus: "".to_string(),
recipients,
encryption,
to: vec![("".to_string(), contact.get_addr().to_string())],
past_members: vec![],
member_fingerprints: vec![],
member_timestamps: vec![],
timestamp,
loaded: Loaded::Mdn {
rfc724_mid,
additional_msg_ids,
},
in_reply_to: String::default(),
references: Vec::new(),
req_mdn: false,
attach_selfavatar: false,
webxdc_topic: None,
pre_message_mode: PreMessageMode::None,
};
Ok(res)
}
/// Returns whether own Autocrypt key should be attached to this MDN
/// and if so, records the attachment.
///
@@ -860,15 +925,15 @@ impl MimeFactory {
/// so that contacts we only read messages from
/// still learn our current key and relay list
/// and will likely re-gossip it to group chats.
async fn update_mdn_pubkey_attachment(
context: &Context,
encryption_pubkeys: &[SignedPublicKey],
) -> Result<bool> {
async fn update_mdn_pubkey_attachment(&self, context: &Context) -> Result<bool> {
let Encryption::Asymmetric { encryption_pubkeys } = &self.encryption else {
return Ok(false);
};
debug_assert!(
encryption_pubkeys.len() <= 1,
"MDNs have at most one recipient key; own key is only added at encryption time"
);
let [ref key] = encryption_pubkeys[..] else {
let [(_, ref key)] = encryption_pubkeys[..] else {
return Ok(false);
};
let fingerprint = key.dc_fingerprint().hex();
@@ -950,69 +1015,80 @@ impl MimeFactory {
}
fn grpimage(&self) -> Option<String> {
let cmd = self.msg.param.get_cmd();
match &self.loaded {
Loaded::Message { chat, msg } => {
let cmd = msg.param.get_cmd();
match cmd {
SystemMessage::MemberAddedToGroup => {
return self.chat.param.get(Param::ProfileImage).map(Into::into);
match cmd {
SystemMessage::MemberAddedToGroup => {
return chat.param.get(Param::ProfileImage).map(Into::into);
}
SystemMessage::GroupImageChanged => {
return msg.param.get(Param::Arg).map(Into::into);
}
_ => {}
}
if msg
.param
.get_bool(Param::AttachChatAvatarAndDescription)
.unwrap_or_default()
{
return chat.param.get(Param::ProfileImage).map(Into::into);
}
None
}
SystemMessage::GroupImageChanged => {
return self.msg.param.get(Param::Arg).map(Into::into);
}
_ => {}
Loaded::Mdn { .. } => None,
}
if self
.msg
.param
.get_bool(Param::AttachChatAvatarAndDescription)
.unwrap_or_default()
{
return self.chat.param.get(Param::ProfileImage).map(Into::into);
}
None
}
async fn subject_str(&self, context: &Context) -> Result<String> {
let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject);
let subject = match &self.loaded {
Loaded::Message { chat, msg } => {
let quoted_msg_subject = msg.quoted_message(context).await?.map(|m| m.subject);
if !self.msg.subject.is_empty() {
return Ok(self.msg.subject.clone());
}
if !msg.subject.is_empty() {
return Ok(msg.subject.clone());
}
if (self.chat.typ == Chattype::Group || self.chat.typ == Chattype::OutBroadcast)
&& quoted_msg_subject.is_none_or_empty()
{
let re = if self.in_reply_to.is_empty() {
""
} else {
"Re: "
};
return Ok(format!("{}{}", re, self.chat.name));
}
if (chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast)
&& quoted_msg_subject.is_none_or_empty()
{
let re = if self.in_reply_to.is_empty() {
""
} else {
"Re: "
};
return Ok(format!("{}{}", re, chat.name));
}
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
self.chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
let self_name = match Self::should_attach_profile_data(msg) {
true => context.get_config(Config::Displayname).await?,
false => None,
};
let self_name = &match self_name {
Some(name) => name,
None => context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
};
stock_str::subject_for_new_contact(context, self_name)
}
Loaded::Mdn { .. } => "Receipt Notification".to_string(), // untranslated to no reveal sender's language
};
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
let self_name = match Self::should_attach_profile_data(&self.msg) {
true => context.get_config(Config::Displayname).await?,
false => None,
};
let self_name = &match self_name {
Some(name) => name,
None => context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
};
Ok(stock_str::subject_for_new_contact(context, self_name))
Ok(subject)
}
pub fn recipients(&self) -> Vec<String> {
@@ -1089,10 +1165,10 @@ impl MimeFactory {
));
}
if self.chat.typ == Chattype::Group {
if !self.member_timestamps.is_empty()
&& !self.chat.member_list_is_stale(context).await?
{
if let Loaded::Message { chat, .. } = &self.loaded
&& chat.typ == Chattype::Group
{
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
headers.push((
"Chat-Group-Member-Timestamps",
mail_builder::headers::raw::Raw::new(
@@ -1144,25 +1220,29 @@ impl MimeFactory {
}
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if context.get_config_bool(Config::Bot).await? {
if let Loaded::Mdn { .. } = self.loaded {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
));
} else if context.get_config_bool(Config::Bot).await? {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
));
}
if self.chat.typ == Chattype::OutBroadcast || self.chat.typ == Chattype::InBroadcast {
if let Loaded::Message { msg, chat } = &self.loaded
&& (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
{
headers.push((
"Chat-List-ID",
mail_builder::headers::text::Text::new(format!(
"{} <{}>",
self.chat.name, self.chat.grpid
))
.into(),
mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
.into(),
));
if self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
&& let Some(secret) = self.msg.param.get(PARAM_BROADCAST_SECRET)
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
&& let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
{
headers.push((
"Chat-Broadcast-Secret",
@@ -1171,18 +1251,22 @@ impl MimeFactory {
}
}
if let Some(original_rfc724_mid) = self.msg.param.get(Param::TextEditFor) {
headers.push((
"Chat-Edit",
mail_builder::headers::message_id::MessageId::new(original_rfc724_mid.to_string())
if let Loaded::Message { msg, .. } = &self.loaded {
if let Some(original_rfc724_mid) = msg.param.get(Param::TextEditFor) {
headers.push((
"Chat-Edit",
mail_builder::headers::message_id::MessageId::new(
original_rfc724_mid.to_string(),
)
.into(),
));
} else if let Some(rfc724_mid_list) = self.msg.param.get(Param::DeleteRequestFor) {
headers.push((
"Chat-Delete",
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
.into(),
));
));
} else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) {
headers.push((
"Chat-Delete",
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
.into(),
));
}
}
headers.push((
@@ -1219,12 +1303,14 @@ impl MimeFactory {
// Add ephemeral timer for non-MDN messages.
// For MDNs it does not matter because they are not visible
// and ignored by the receiver.
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
headers.push((
"Ephemeral-Timer",
mail_builder::headers::raw::Raw::new(duration.to_string()).into(),
));
if let Loaded::Message { msg, .. } = &self.loaded {
let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
headers.push((
"Ephemeral-Timer",
mail_builder::headers::raw::Raw::new(duration.to_string()).into(),
));
}
}
Ok(headers)
@@ -1232,14 +1318,19 @@ impl MimeFactory {
/// Helper function render the messages that are not queued.
///
/// Used only for tests.
#[cfg(test)]
pub async fn render(self, context: &Context) -> Result<RenderedEmail> {
/// Used for MDNs because they are fully rendered and sent in one go,
/// rather than first creating a [`QueuedMail`] and sending it later.
pub async fn render(self, context: &Context, from_addr: &str) -> Result<RenderedEmail> {
// Does not matter, we are not going to return the QueuedMail.
let bcc_self = false;
let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let (queued_mail, _side_effects) =
Box::pin(self.into_queued_mail(context, bcc_self)).await?;
render_queued_mail_with_context(queued_mail, context).await
let rendered_mail =
render_queued_mail(queued_mail, &public_key, &secret_key, from_addr.to_string())?;
Ok(rendered_mail)
}
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
@@ -1250,15 +1341,18 @@ impl MimeFactory {
context: &Context,
bcc_self: bool,
) -> Result<ToBeQueuedMail> {
let rfc724_mid = match &self.pre_message_mode {
PreMessageMode::Pre { .. } => {
if self.msg.pre_rfc724_mid.is_empty() {
create_outgoing_rfc724_mid()
} else {
self.msg.pre_rfc724_mid.clone()
let rfc724_mid = match &self.loaded {
Loaded::Message { msg, .. } => match &self.pre_message_mode {
PreMessageMode::Pre { .. } => {
if msg.pre_rfc724_mid.is_empty() {
create_outgoing_rfc724_mid()
} else {
msg.pre_rfc724_mid.clone()
}
}
}
_ => self.msg.rfc724_mid.clone(),
_ => msg.rfc724_mid.clone(),
},
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
};
let subject_str = self.subject_str(context).await?;
@@ -1268,44 +1362,61 @@ impl MimeFactory {
let is_encrypted = self.will_be_encrypted();
let RenderedMessage {
main_part,
mut parts,
last_added_location_timestamp,
avatar_is_attached,
sync_ids_to_delete,
} = self
.render_message(context, &mut headers, &grpimage, is_encrypted)
.await?;
let side_effects: Option<QueueSideEffects>;
let side_effects = QueueSideEffects {
chat_id: self.msg.chat_id,
avatar_is_attached,
sync_ids_to_delete,
last_added_location_timestamp,
subject: subject_str,
};
let message: MimePart<'static> = match &self.loaded {
Loaded::Message { msg, .. } => {
let msg = msg.clone();
let RenderedMessage {
main_part,
mut parts,
last_added_location_timestamp,
avatar_is_attached,
sync_ids_to_delete,
} = self
.render_message(context, &mut headers, &grpimage, is_encrypted)
.await?;
let message: MimePart<'static> = if parts.is_empty() {
// Single part, render as regular message.
main_part
} else {
parts.insert(0, main_part);
side_effects = Some(QueueSideEffects {
chat_id: msg.chat_id,
avatar_is_attached,
sync_ids_to_delete,
last_added_location_timestamp,
subject: subject_str,
});
// Multiple parts, render as multipart.
if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
MimePart::new("multipart/report; report-type=multi-device-sync", parts)
} else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
MimePart::new("multipart/report; report-type=status-update", parts)
} else {
MimePart::new("multipart/mixed", parts)
if parts.is_empty() {
// Single part, render as regular message.
main_part
} else {
parts.insert(0, main_part);
// Multiple parts, render as multipart.
if msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
MimePart::new("multipart/report; report-type=multi-device-sync", parts)
} else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
MimePart::new("multipart/report; report-type=status-update", parts)
} else {
MimePart::new("multipart/mixed", parts)
}
}
}
Loaded::Mdn { .. } => {
side_effects = None;
self.render_mdn()?
}
};
let should_attach_pubkey = true;
let should_attach_pubkey = match &self.loaded {
Loaded::Message { .. } => true,
Loaded::Mdn { .. } => self.update_mdn_pubkey_attachment(context).await?,
};
let is_post_message = self.pre_message_mode == PreMessageMode::Post;
let is_securejoin_message = self.msg.param.get_cmd() == SystemMessage::SecurejoinMessage;
let is_securejoin_message = match &self.loaded {
Loaded::Message { msg, .. } => msg.param.get_cmd() == SystemMessage::SecurejoinMessage,
Loaded::Mdn { .. } => false,
};
// Disable compression for SecureJoin to ensure
// there are no compression side channels
@@ -1323,70 +1434,79 @@ impl MimeFactory {
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
let now = time();
if !should_hide_recipients(&self.msg, &self.chat) {
for (addr, key) in encryption_pubkeys {
let fingerprint = key.dc_fingerprint().hex();
let cmd = self.msg.param.get_cmd();
if is_post_message {
continue;
}
match &self.loaded {
Loaded::Message { chat, msg } => {
if !should_hide_recipients(msg, chat) {
for (addr, key) in encryption_pubkeys {
let fingerprint = key.dc_fingerprint().hex();
let cmd = msg.param.get_cmd();
if is_post_message {
continue;
}
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
|| multiple_recipients && {
let gossiped_timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT timestamp
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
|| multiple_recipients && {
let gossiped_timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT timestamp
FROM gossip_timestamp
WHERE chat_id=? AND fingerprint=?",
(self.chat.id, &fingerprint),
)
.await?;
(chat.id, &fingerprint),
)
.await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
//
// If current time is in the past compared to
// `gossiped_timestamp`, we also gossip because
// either the `gossiped_timestamp` or clock is wrong.
gossip_period == 0
|| gossiped_timestamp
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
};
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
//
// If current time is in the past compared to
// `gossiped_timestamp`, we also gossip because
// either the `gossiped_timestamp` or clock is wrong.
gossip_period == 0
|| gossiped_timestamp
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
};
if !should_do_gossip {
continue;
}
if !should_do_gossip {
continue;
}
let header = Aheader {
addr: addr.clone(),
public_key: key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference,
}
.to_string();
let header = Aheader {
addr: addr.clone(),
public_key: key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference,
}
.to_string();
headers.push((
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header).into(),
));
headers.push((
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header).into(),
));
context
.sql
.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
context
.sql
.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (chat_id, fingerprint)
DO UPDATE SET timestamp=excluded.timestamp",
(self.chat.id, &fingerprint, now),
)
.await?;
(chat.id, &fingerprint, now),
)
.await?;
}
}
}
Loaded::Mdn { .. } => {
// Never gossip in MDNs.
}
}
}
let is_encrypted = self.will_be_encrypted();
let display_name = if is_securejoin_message && !is_encrypted {
// Unencrypted securejoin messages should _not_ include the display name.
"".to_string()
@@ -1394,13 +1514,25 @@ impl MimeFactory {
self.from_displayname.clone()
};
let is_mdn = matches!(self.loaded, Loaded::Mdn { .. });
let should_sign = true;
let message = if is_encrypted {
add_headers_to_encrypted_part(message, headers)
} else if is_mdn {
// Never add outer multipart/mixed wrapper to MDN
// as multipart/report Content-Type is used to recognize MDNs
// by Delta Chat receiver and Chatmail servers
// allowing them to be unencrypted and not contain Autocrypt header
// without resetting Autocrypt encryption or triggering Chatmail filter
// that normally only allows encrypted mails.
message
} else {
// Unencrypted message.
let message = if self.msg.param.get_cmd() == SystemMessage::SecurejoinMessage
let message = if let Loaded::Message { msg, .. } = &self.loaded
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
&& matches!(
self.msg.param.get(Param::Arg),
msg.param.get(Param::Arg),
Some("vc-request") | Some("vg-request")
) {
// Workaround for legacy SecureJoin {vc,vg}-request messages.
@@ -1413,7 +1545,12 @@ impl MimeFactory {
message
};
add_headers_to_part(message, headers)
headers.iter().fold(message, |message, (header, value)| {
debug_assert_ne!(*header, "from");
debug_assert_ne!(*header, "message-id");
debug_assert_ne!(*header, "autocrypt");
message.header(*header, value.clone())
})
};
let raw_message = part_to_bytes(message);
let recipients = self.recipients();
@@ -1424,7 +1561,7 @@ impl MimeFactory {
display_name,
encryption: self.encryption.into_queued_encryption(),
should_attach_pubkey,
should_sign: true,
should_sign,
should_compress,
recipients,
sent_to: Vec::new(),
@@ -1435,10 +1572,14 @@ impl MimeFactory {
/// Returns MIME part with a `message.kml` attachment.
fn get_message_kml_part(&self) -> Option<MimePart<'static>> {
let latitude = self.msg.param.get_float(Param::SetLatitude)?;
let longitude = self.msg.param.get_float(Param::SetLongitude)?;
let Loaded::Message { msg, .. } = &self.loaded else {
return None;
};
let kml_file = location::get_message_kml(self.msg.timestamp_sort, latitude, longitude);
let latitude = msg.param.get_float(Param::SetLatitude)?;
let longitude = msg.param.get_float(Param::SetLongitude)?;
let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude);
let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_file)
.attachment("message.kml");
Some(part)
@@ -1450,8 +1591,12 @@ impl MimeFactory {
&self,
context: &Context,
) -> Result<Option<(MimePart<'static>, i64)>> {
let Loaded::Message { msg, .. } = &self.loaded else {
return Ok(None);
};
let Some((kml_content, last_added_location_timestamp)) =
location::get_kml(context, self.msg.chat_id).await?
location::get_kml(context, msg.chat_id).await?
else {
return Ok(None);
};
@@ -1468,8 +1613,11 @@ impl MimeFactory {
grpimage: &Option<String>,
is_encrypted: bool,
) -> Result<RenderedMessage> {
let chat = self.chat.clone();
let msg = self.msg.clone();
let Loaded::Message { chat, msg } = &self.loaded else {
bail!("Attempt to render MDN as a message");
};
let chat = chat.clone();
let msg = msg.clone();
let command = msg.param.get_cmd();
let mut placeholdertext = None;
@@ -2005,6 +2153,59 @@ impl MimeFactory {
})
}
/// Render an MDN
fn render_mdn(&mut self) -> Result<MimePart<'static>> {
// RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report
let Loaded::Mdn {
rfc724_mid,
additional_msg_ids,
} = &self.loaded
else {
bail!("Attempt to render a message as MDN");
};
// first body part: always human-readable, always REQUIRED by RFC 6522.
// untranslated to no reveal sender's language.
// moreover, translations in unknown languages are confusing, and clients may not display them at all
let text_part = MimePart::new("text/plain", "This is a receipt notification.");
let mut message = MimePart::new(
"multipart/report; report-type=disposition-notification",
vec![text_part],
);
// second body part: machine-readable, always REQUIRED by RFC 6522
//
// We do not include the Final-Recipient field.
// According to <https://datatracker.ietf.org/doc/html/rfc8098#section-3.2.4>
// it MUST be present and be the address on which original message was received,
// but practically it is not going to be used.
let message_text2 = format!(
"Original-Message-ID: <{rfc724_mid}>\r\n\
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
);
let extension_fields = if additional_msg_ids.is_empty() {
"".to_string()
} else {
"Additional-Message-IDs: ".to_string()
+ &additional_msg_ids
.iter()
.map(|mid| render_rfc724_mid(mid))
.collect::<Vec<String>>()
.join(" ")
+ "\r\n"
};
message.add_part(MimePart::new(
"message/disposition-notification",
message_text2 + &extension_fields,
));
Ok(message)
}
pub fn will_be_encrypted(&self) -> bool {
self.encryption.is_encrypted()
}
@@ -2034,26 +2235,20 @@ pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
)
}
fn add_headers_to_part(
message: MimePart<'static>,
headers: Vec<(&'static str, HeaderType<'static>)>,
) -> MimePart<'static> {
headers
.into_iter()
.fold(message, |message, (header, value)| {
debug_assert_ne!(header, "from");
debug_assert_ne!(header, "message-id");
debug_assert_ne!(header, "autocrypt");
message.header(header, value)
})
}
fn add_headers_to_encrypted_part(
message: MimePart<'static>,
protected_headers: Vec<(&'static str, HeaderType<'static>)>,
) -> MimePart<'static> {
// Store protected headers in the inner message.
let mut message = add_headers_to_part(message, protected_headers);
let mut message: MimePart<'static> =
protected_headers
.into_iter()
.fold(message, |message, (header, value)| {
debug_assert_ne!(header, "from");
debug_assert_ne!(header, "message-id");
debug_assert_ne!(header, "autocrypt");
message.header(header, value)
});
// Set the appropriate Content-Type for the inner message
for (h, v) in &mut message.headers {
@@ -2241,127 +2436,6 @@ pub(crate) async fn symm_encrypted_securejoin_message(
Ok(queued_mail)
}
/// Returns an MDN body.
fn mdn_body(rfc724_mid: &str, additional_msg_ids: Vec<String>) -> MimePart<'static> {
// RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report
// first body part: always human-readable, always REQUIRED by RFC 6522.
// untranslated to no reveal sender's language.
// moreover, translations in unknown languages are confusing, and clients may not display them at all
let text_part = MimePart::new("text/plain", "This is a receipt notification.");
// second body part: machine-readable, always REQUIRED by RFC 6522
//
// We do not include the Final-Recipient field.
// According to <https://datatracker.ietf.org/doc/html/rfc8098#section-3.2.4>
// it MUST be present and be the address on which original message was received,
// but practically it is not going to be used.
let message_text2 = format!(
"Original-Message-ID: <{rfc724_mid}>\r\n\
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
);
let extension_fields = if additional_msg_ids.is_empty() {
"".to_string()
} else {
"Additional-Message-IDs: ".to_string()
+ &additional_msg_ids
.iter()
.map(|mid| render_rfc724_mid(mid))
.collect::<Vec<String>>()
.join(" ")
+ "\r\n"
};
MimePart::new(
"multipart/report; report-type=disposition-notification",
vec![
text_part,
MimePart::new(
"message/disposition-notification",
message_text2 + &extension_fields,
),
],
)
}
pub(crate) async fn mdn(
context: &Context,
contact_id: ContactId,
rfc724_mid: &str,
additional_rfc724_mids: Vec<String>,
) -> Result<QueuedMail> {
let contact = Contact::get_by_id(context, contact_id).await?;
let timestamp = time();
let addr = contact.get_addr().to_string();
let recipients: Vec<String>;
let encryption_pubkeys = if contact_id == ContactId::SELF {
recipients = Vec::new();
Vec::new()
} else if let Some(key) = contact.public_key(context).await? {
recipients = relay_addrs(&key, &addr);
vec![key]
} else {
warn!(context, "Contact {contact_id} has no key, sending to self.");
// Encryption key for the contact is not available, sending MDN to self only.
recipients = Vec::new();
Vec::new()
};
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
.context("Failed to convert timestamp to DateTime")?
.to_rfc2822();
let headers: Vec<(&'static str, HeaderType<'static>)> = vec![
("Date", mail_builder::headers::raw::Raw::new(date).into()),
(
"To",
mail_builder::headers::address::Address::new_list(vec![Address::new_address(
None::<&'static str>,
contact.get_addr().to_string(),
)])
.into(),
),
// Subject is untranslated to not reveal sender's language.
(
"Subject",
mail_builder::headers::text::Text::new("Receipt Notification".to_string()).into(),
),
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
(
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
),
(
"Chat-Version",
mail_builder::headers::raw::Raw::new("1.0").into(),
),
];
let message = mdn_body(rfc724_mid, additional_rfc724_mids);
let should_attach_pubkey =
MimeFactory::update_mdn_pubkey_attachment(context, &encryption_pubkeys).await?;
let message = add_headers_to_encrypted_part(message, headers);
let raw_message = part_to_bytes(message);
let encryption = QueuedEncryption::Asymmetric { encryption_pubkeys };
let queued_mdn = QueuedMail {
raw_message,
rfc724_mid: create_outgoing_rfc724_mid(),
display_name: String::new(),
encryption,
should_attach_pubkey,
should_sign: true,
should_compress: true,
recipients,
sent_to: Vec::new(),
bcc_self,
};
Ok(queued_mdn)
}
/// Returns the body of a keyupdate message, shaped like a receipt notification.
///
/// The shape is what every core goes by, as a keyupdate carries no marker:
@@ -2369,7 +2443,7 @@ pub(crate) async fn mdn(
/// while a plain text body would end up in a contact request.
/// The report deliberately names no original message, see [`crate::keyupdate`].
fn keyupdate_body() -> MimePart<'static> {
// Human-readable first part as RFC 6522 requires.
// Human-readable first part as RFC 6522 requires, untranslated like in `render_mdn`.
let text_part = MimePart::new(
"text/plain",
"This message updates the sender's encryption key and relay list.",
+22 -44
View File
@@ -281,12 +281,8 @@ async fn test_subject_mdn() {
assert_eq!("Re: Hello, Bob", mf.subject_str(t).await.unwrap());
}
/// Tests that MDN for unencrypted message can be created without throwing an error.
///
/// We do not send unencrypted MDNs, but do not want SMTP loop to get stuck
/// if we somehow request the creation of unencrypted MDN.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_create_unencrypted() -> Result<()> {
async fn test_mdn_create_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.allow_unencrypted().await?;
@@ -301,43 +297,21 @@ async fn test_mdn_create_unencrypted() -> Result<()> {
.await?;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// MDN for unencrypted message.
// Should not happen, but should also not throw an error.
// MDN for unencrypted message is not encrypted.
let mut msg = Message::new(Viewtype::Text);
let chat_alice = alice.create_email_chat(&bob).await.id;
let sent = alice.send_msg(chat_alice, &mut msg).await;
let rcvd = bob.recv_msg(&sent).await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
// Only sending MDN to self (if BCC-self is enabled) because address-contact recipient has no key.
assert!(queued_mdn.recipients.is_empty());
// MDNs are always encrypted, even if requested for unencrypted message.
assert!(queued_mdn.encryption.is_encrypted());
bob.assert_warn("has no key, sending to self").await;
Ok(())
}
/// Tests that MDNs sent in reply to encrypted messages are encrypted
/// and MDNs for unencrypted messages are not created.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_create_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice
.set_config(Config::Displayname, Some("Alice Exampleorg"))
.await?;
let bob = tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
.await?;
bob.set_config(Config::Selfstatus, Some("Bob Examplenet"))
.await?;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
let mimefactory =
MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
assert!(!mimefactory.will_be_encrypted());
let bob_addr = bob.get_primary_self_addr().await?;
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
assert!(!rendered_msg.message.contains("Bob Examplenet"));
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
let bob_alice_contact = bob.add_or_lookup_contact(&alice).await;
assert_eq!(bob_alice_contact.get_authname(), "Alice Exampleorg");
@@ -345,9 +319,10 @@ async fn test_mdn_create_encrypted() -> Result<()> {
let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let queued_mdn = mdn(&bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
assert!(queued_mdn.encryption.is_encrypted());
let rendered_msg = render_queued_mail_with_context(queued_mdn, &bob).await?;
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
assert!(mimefactory.will_be_encrypted());
let bob_addr = bob.get_primary_self_addr().await?;
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
assert!(!rendered_msg.message.contains("Bob Examplenet"));
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
@@ -366,7 +341,6 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
// Bob's key gets a second relay address and Alice merges the newer key.
let bob_secret_key = load_self_secret_key(bob).await?;
let bob_public_key = secret_key_to_public_key(
bob,
bob_secret_key,
u32::try_from(time())? + 100,
"bob@example.net",
@@ -374,8 +348,8 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
)?;
import_public_key(alice, &bob_public_key).await?;
let queued_mdn = mdn(alice, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
let mut recipients = queued_mdn.recipients;
let mimefactory = MimeFactory::from_mdn(alice, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
let mut recipients = mimefactory.recipients();
recipients.sort();
assert_eq!(recipients, vec!["bob@example.net", "bob@relay2.example"]);
@@ -389,8 +363,9 @@ async fn test_mdn_autocrypt_throttle() -> Result<()> {
alice: &TestContext,
rcvd: &Message,
) -> Result<bool> {
let queued_mdn = mdn(bob, rcvd.from_id, &rcvd.rfc724_mid, vec![]).await?;
let rendered_msg = render_queued_mail_with_context(queued_mdn, bob).await?;
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
let addr = bob.get_primary_self_addr().await?;
let rendered_msg = mf.render(bob, &addr).await?;
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
Ok(mime.autocrypt_fingerprint.is_some())
}
@@ -671,7 +646,8 @@ async fn test_render_reply() {
let recipients = mimefactory.recipients();
assert_eq!(recipients, vec!["charlie@example.net"]);
let rendered_msg = mimefactory.render(t).await.unwrap();
let addr = t.get_primary_self_addr().await.unwrap();
let rendered_msg = mimefactory.render(t, &addr).await.unwrap();
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
assert_eq!(
@@ -1123,6 +1099,7 @@ To: <bob@example.net>
Subject: Message from alice@example.org
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
Content-Transfer-Encoding: 7bit
Hello!"#
@@ -1174,6 +1151,7 @@ To: <bob@example.net>
Subject: Message from alice@example.org
References: <MESSAGE_ID@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
--BOUNDARY
+4 -5
View File
@@ -931,10 +931,9 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if self.decryption_error.is_none()
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
&& self.wants_mdn
&& self.was_encrypted() // Do not send MDNs for unencrypted messages.
&& self.incoming
&& let Some(part) = self.parts.last_mut()
&& self.wants_mdn
&& self.incoming
&& let Some(part) = self.parts.last_mut()
{
part.param.set_int(Param::WantsMdn, 1);
}
@@ -948,7 +947,7 @@ impl MimeMessage {
typ: Viewtype::Text,
..Default::default()
};
if self.wants_mdn && self.was_encrypted() && self.incoming {
if self.wants_mdn && self.incoming {
part.param.set_int(Param::WantsMdn, 1);
}
if let Some(ref subject) = self.get_subject()
+2 -4
View File
@@ -410,8 +410,7 @@ impl ProxyConfig {
};
let tcp_stream =
crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
.await
.context("Failed to connect to HTTP proxy")?;
.await?;
let auth = if let Some((username, password)) = &http_config.user_password {
Some((username.as_str(), password.as_str()))
} else {
@@ -430,8 +429,7 @@ impl ProxyConfig {
let tcp_stream =
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
.await
.context("Failed to connect to HTTPS proxy")?;
.await?;
let use_sni = true;
let tls_stream = wrap_rustls(
&hostname,
+58 -6
View File
@@ -1,5 +1,6 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::io::Cursor;
@@ -83,13 +84,62 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
/// Selects a subkey of the public key to use for encryption.
///
/// Returns `None` if the public key cannot be used for encryption.
/// The key is selected according to
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
/// If multiple keys are available, the one that will expire sooner is selected.
///
/// TODO: take key flags and expiration dates into account
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
/// Returns `None` if the public key cannot be used for encryption.
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
key.public_subkeys
.iter()
.find(|subkey| subkey.algorithm().can_encrypt())
.filter(|subkey| subkey.algorithm().can_encrypt())
.filter_map(|subkey| {
let signature = subkey.signatures.first()?;
let key_flags = signature.key_flags();
if !key_flags.encrypt_comms() {
return None;
}
if let Some(expiration_duration) = signature
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0)
&& now
> subkey
.created_at()
.as_secs()
.saturating_add(expiration_duration.as_secs())
{
// Key is expired.
return None;
}
Some((subkey, signature))
})
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
match (
signature1
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
signature2
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(expiration1), Some(expiration2)) => (subkey1
.created_at()
.as_secs()
.saturating_add(expiration1.as_secs()))
.cmp(
&(subkey2
.created_at()
.as_secs()
.saturating_add(expiration2.as_secs())),
),
}
})
.map(|(subkey, _signature)| subkey)
}
/// Version of SEIPD packet to use.
@@ -151,10 +201,11 @@ pub fn pk_encrypt(
) -> Result<String> {
tokio::task::block_in_place(|| {
let mut rng = thread_rng();
let now = pgp::types::Timestamp::now();
let pkeys = public_keys_for_encryption
.iter()
.filter_map(select_pk_for_encryption);
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
let msg = MessageBuilder::from_bytes("", plain);
let encoded_msg = match seipd_version {
@@ -485,7 +536,8 @@ pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<Strin
/// Returns true if the key can be encrypted to, i.e. has an encryption subkey.
pub(crate) fn pubkey_can_encrypt(public_key: &SignedPublicKey) -> bool {
select_pk_for_encryption(public_key).is_some()
let now = pgp::types::Timestamp::now();
select_pk_for_encryption(now.as_secs(), public_key).is_some()
}
/// Returns true if public key advertises SEIPDv2 feature.
+82
View File
@@ -423,3 +423,85 @@ async fn test_securejoin_pqc_joiner() {
tcm.execute_securejoin(bob, pqc).await;
}
/// Tests that public subkey selection for encryption follows Autocrypt 2 rules.
///
/// If there is an expiring subkey, it is preferred, otherwise non-expiring subkey is selected.
/// Non-encryption subkeys such as RSA subkey for authentication are ignored.
#[test]
fn test_select_pk_for_encryption() {
// Public key generated with GnuPG 2.4.9 with the following subkeys:
// 1. Auth-only RSA subkey (92E762B9084CA740).
// 2. Expired Curve25519 encryption subkey with 1-day expiration (C8F382BD0F35C49E)
// 3. Ed25519 signing subkey (F177AC3118F923CC).
// 4. Curve25519 encryption subkey with fingerprint (36188C6FFC8E267B)
// 5. Curve25519 encryption subkey with 1 year expiration, valid in the beginning of 2008, with key ID 9223FCEE7546CDE7
// 6. Curve25519 encryption subkey with no expiration (FD2C0567967223D8).
// Primary key is an Ed25519 not expiring key.
// Key 4 is the one that should be selected.
// Subkey 4 fingerprint.
let expected_fallback_fingerprint = "cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b";
// Subkey 5 fingerprint, should be preferred to fallback when not expired.
let expected_expiring_fingerprint = "5133fab157c4a46ca41f6dc39223fcee7546cde7";
let alice_tpk_asc = "
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMERvfcPBYJKwYBBAHaRw8BAQdAimvPsr7NdJ4dBoFPySwhpTQqoYOoHL3AzfE7
mGWQOOC0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBCi19Yqv
ugVVkhyHgicqAms0FFoiBQJG99w8AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
AAoJECcqAms0FFoiUGEA/3VZMBCoRq0ZpHarzmvzgdZCoL3r3m9en/eZScFzxITx
AP42Mn27r0SOKwIln0VcPTdAQCk49mBW/EX3CMOlLPU3DLkBjQRG99w8AQwArsYe
Jkdl6sSM/hfoEw0vgx/RdUBQ6QRYi1uc0UUNlIGy8mlczLFdkD3JF/hGocjPvt45
XAQoK110zAkZlfpFRqNT1M/IC68Er8rLkYPC4OeFh6W4Iyn17fcUanP0lf8em/jh
Vffvgy8sFOMdO235lvFA3txNA98s4fHdmU3PScyd1hc3C4M0yP83LnYyWt4X59Xc
E/Om5Dm458eKCSeYkLI6752W0mXsBxSi3/dLn0XeuNRpgmKxSkm562FHOFaLbKtR
Y5hobAI9PkNcVgRxZZvWQls5PHTZWjqngF21lKlaLfdqZ/Uae1i2hzZOihgitL43
Le00qwNBi5hKYMeuDnHaQrLmb+A+0/IAEE4Ub+TkZhzI+2ZP2k1cAT0qZmZi0w+q
xqP9INk0hY/oZFCnV2wkHN7zvQmVlUIcQ2rmfbafK1yiEL1qeGT96zyjbdXeGPqJ
3O9h+YIG38JMRJBijsFujUN34Z546zS/kzOPXsz/WlGUMjwu8n5s5uf2TFyFABEB
AAGIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsgAAoJECcq
Ams0FFoiCOUBAPafRLDpWN9iT4hcCXjESf1Hw5KNVkJpwfzPfu2H9BkMAQCaHhKg
pq9ywH4pyOHZCPV8P2ywkyn+EsjBC3fG+GBBBbg4BEb33DwSCisGAQQBl1UBBQEB
B0DfI8AJFT3nWa6ZXLkHSf7W8W7S6AWIO7LAcjoyHwb8CwMBCAeIfgQYFgoAJhYh
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMBQkAAVGAAAoJECcqAms0FFoi
U5EA/3G74HRwIMJlNOEW5gkYYV5KJW2qgtMfxHCUjoHvNWU1AQCHt/bLU2aviAiS
of1R43qojxKUqzzoi8lYRQ+1sYhvB7gzBEb33DwWCSsGAQQB2kcPAQEHQKZXUJ7s
xqH3kVcMnhasw6DrFMwCxHDdj+qvkg8r/DvtiO8EGBYKACAWIQQotfWKr7oFVZIc
h4InKgJrNBRaIgUCRvfcPAIbAgCBCRAnKgJrNBRaInYgBBkWCgAdFiEEI8hstnVQ
9sgylIYg8XesMRj5I8wFAkb33DwACgkQ8XesMRj5I8xo6QEAu4o/TyEZwFcyqZpw
LEo9vTLCsc7fo0nx0ssiP6FyV5cBAOWal1DznDhsXWCNt+U8UaafXsU2DTV51KaD
VBOVFo4CyeQBAMirIjXV5PbUV674TNLhYl2s0jTtNz+GKtOjSdZuRm1mAPwPG6ya
K1b7iMRdBT92gNZMw30LbtcXmttCxpZAwr9lBLg4BEb33DwSCisGAQQBl1UBBQEB
B0C5fFb4WTHoIoI6ou/31+1N1wn8ghsSkUVzpbtv/aTkegMBCAeIeAQYFgoAIBYh
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMAAoJECcqAms0FFoi8z8BALPL
7V0ICLEY5YSUa4lQ2rjiXOcVTlWkG3h4TATPrr08AP9tIAQIE0o50IGdQAcKJoTn
Lyxnf2wfjZ16vL3JLLjSBrg4BEb33DwSCisGAQQBl1UBBQEBB0DXDrcGrnuLjAUO
eo/t8MQNOe+ZKYSDPGTkO7iM5IloSAMBCAeIfgQYFgoAJhYhBCi19YqvugVVkhyH
gicqAms0FFoiBQJG99w8AhsMBQkB4TOAAAoJECcqAms0FFoivQIA/2PcZ1vcImAa
7ldPY00JkcW6WlSSd6yOIZsVa4TdA1FiAP42gOji+4RrLps2+NX6L1znSc8EJBXo
RMbND/CZQWXfA7g4BEb33DwSCisGAQQBl1UBBQEBB0BHxCvo5zuygw2XiluYNobx
7iFJqlmCkjekKyoVFquKHQMBCAeIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0
FFoiBQJG99w8AhsMAAoJECcqAms0FFoirSMA/0gVP98sPFga+UhQ3uJxJw5bO2Rs
7hxVk6aPREWgBYg1AQCT7AE8m7j17SP/1fl8OjpxsQQmCJyv2wNcP48OfKGOCA==
=AXAi
-----END PGP PUBLIC KEY BLOCK-----
";
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
let now = pgp::types::Timestamp::now();
let encryption_subkey = select_pk_for_encryption(now.as_secs(), &alice_tpk).unwrap();
assert_eq!(
encryption_subkey.fingerprint().to_string().as_str(),
expected_fallback_fingerprint
);
// In the beginning of 2008 expiring subkey is not expired yet and should be used.
let encryption_subkey = select_pk_for_encryption(1199149200, &alice_tpk).unwrap();
assert_eq!(
encryption_subkey.fingerprint().to_string().as_str(),
expected_expiring_fingerprint
);
}
+9 -5
View File
@@ -1,6 +1,6 @@
//! Handle plain text together with some attributes.
use regex::regex;
use std::sync::LazyLock;
use crate::simplify::remove_message_footer;
@@ -25,8 +25,12 @@ impl PlainText {
/// Convert plain text to HTML.
/// The function handles quotes, links, fixed and floating text paragraphs.
pub fn to_html(&self) -> String {
let linkify_mail_re = regex!(r"\b([\w.\-+]+@[\w.\-]+)\b");
let linkify_url_re = regex!(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)");
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
@@ -48,12 +52,12 @@ impl PlainText {
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
let line = line.to_string().replace('\r', "");
let mut line = linkify_mail_re
let mut line = LINKIFY_MAIL_RE
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = linkify_url_re
line = LINKIFY_URL_RE
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
+8 -6
View File
@@ -2,13 +2,13 @@
mod dclogin_scheme;
use std::collections::BTreeMap;
use std::sync::LazyLock;
use anyhow::{Context as _, Result, anyhow, bail, ensure};
pub use dclogin_scheme::LoginOptions;
pub(crate) use dclogin_scheme::login_param_from_login_qr;
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
use regex::regex;
use serde::Deserialize;
use crate::autorelay::login_param_from_host;
@@ -1071,11 +1071,16 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
Qr::from_address(context, name, &addr, None).await
}
static VCARD_NAME_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
/// Extract address for the vcard scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
let name = regex!(r"(?m)^N:([^;]*);([^;\n]*)")
let name = VCARD_NAME_RE
.captures(qr)
.and_then(|caps| {
let last_name = caps.get(1)?.as_str().trim();
@@ -1085,10 +1090,7 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
})
.unwrap_or_default();
let addr = if let Some(cap) = regex!(r"(?m)^EMAIL([^:\n]*):([^;\n]*)")
.captures(qr)
.and_then(|caps| caps.get(2))
{
let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
normalize_address(cap.as_str().trim())?
} else {
bail!("Bad e-mail address");
+4 -5
View File
@@ -483,7 +483,7 @@ pub(crate) async fn receive_imf_inner(
}
let trash = || async {
let msg_ids = vec![insert_tombstone(context, rfc724_mid, false).await?];
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
Ok(Some(ReceivedMsg {
chat_id: ChatId::TRASH,
state: MessageState::Undefined,
@@ -668,15 +668,14 @@ pub(crate) async fn receive_imf_inner(
match res {
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
let needs_delete_job = res == securejoin::HandshakeMessage::Done;
let msg_id = insert_tombstone(context, rfc724_mid, needs_delete_job).await?;
let msg_id = insert_tombstone(context, rfc724_mid).await?;
received_msg = Some(ReceivedMsg {
chat_id: ChatId::TRASH,
state: MessageState::InSeen,
hidden: false,
sort_timestamp: mime_parser.timestamp_sent,
msg_ids: vec![msg_id],
needs_delete_job,
needs_delete_job: res == securejoin::HandshakeMessage::Done,
});
}
securejoin::HandshakeMessage::Propagate => {
@@ -2402,7 +2401,7 @@ async fn handle_edit_delete(
let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
// Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
insert_tombstone(context, rfc724_mid, false).await?;
insert_tombstone(context, rfc724_mid).await?;
continue;
};
+1 -3
View File
@@ -416,9 +416,7 @@ async fn test_escaped_from() {
);
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
assert_eq!(msg.text, "hello");
// MDN request from unencrypted message is ignored.
assert!(msg.param.get_int(Param::WantsMdn).is_none());
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+10 -6
View File
@@ -119,12 +119,16 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
let fingerprint = self_fingerprint(context).await?;
let self_addrs = context.get_self_addrs().await?;
let mut encoded_addrs = self_addrs
.iter()
.map(|addr| utf8_percent_encode(addr, DISALLOWED_CHARACTERS).to_string());
let self_addr_urlencoded = encoded_addrs.next().context("No self addr configured")?;
let encoded_extra_relays: Vec<String> = encoded_addrs.collect();
let self_addr = context.get_primary_self_addr().await?;
let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
let encoded_extra_relays: Vec<String> = context
.get_self_addrs()
.await?
.into_iter()
.filter(|addr| *addr != self_addr)
.map(|addr| utf8_percent_encode(&addr, DISALLOWED_CHARACTERS).to_string())
.collect();
let r_param = if encoded_extra_relays.is_empty() {
"".to_string()
+4 -31
View File
@@ -6,7 +6,6 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::key::self_fingerprint;
use crate::message::rfc724_mid_exists_ext;
use crate::qr::Qr;
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2ee_info_msg};
@@ -1160,12 +1159,12 @@ async fn test_get_securejoin_qr_encoding() -> Result<()> {
let qr = get_securejoin_qr(alice, None).await?;
assert!(
qr.contains("a=jk%25l@example.net"),
"{qr} doesn't contain 'a=jk%25l@example.net'"
qr.contains("a=alice@example.org"),
"{qr} doesn't contain 'a=alice@example.org'"
);
assert!(
qr.contains("r=asdf@example.org,alice@example.org"),
"{qr} doesn't contain 'r=asdf@example.org,alice@example.org'"
qr.contains("r=jk%25l@example.net,asdf@example.org"),
"{qr} doesn't contain 'r=jk%25l@example.net,asdf@example.org'"
);
let qr = check_qr(bob, &qr).await?;
@@ -1564,29 +1563,3 @@ async fn test_deduplicate_member_added() -> Result<()> {
Ok(())
}
/// Tests that a handled join request is also marked as "deleted"
/// in the database, so that if a copy of the request arrives via
/// other relays in the future, then this copy will also be deleted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_join_request_deleted_on_all_relays() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
bob.add_or_lookup_contact_id(alice).await;
join_securejoin(bob, &qr).await?;
let request = bob.pop_sent_msg().await;
alice.recv_msg_trash(&request).await;
let rfc724_mid = Message::load_from_db(bob, request.sender_msg_id)
.await?
.rfc724_mid;
let (_, deleted) = rfc724_mid_exists_ext(alice, &rfc724_mid, "deleted=1")
.await?
.unwrap();
assert!(deleted);
Ok(())
}
+25 -35
View File
@@ -22,6 +22,7 @@ use crate::log::{LogExt, warn};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory;
use crate::mimefactory::MimeFactory;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::scheduler::connectivity::ConnectivityStore;
@@ -68,8 +69,6 @@ impl Smtp {
// separate task to avoid waiting for reply or timeout.
task::spawn(async move { transport.quit().await });
}
self.transport_id = None;
self.from = None;
self.last_success = None;
}
@@ -357,7 +356,7 @@ pub(crate) async fn insert_into_smtp(
queued_msg: &QueuedMail,
) -> Result<()> {
let now = tools::time();
let msg_id = message::insert_tombstone(context, rfc724_mid, false).await?;
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
context
.sql
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
@@ -653,24 +652,6 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
Ok(())
}
async fn delete_mdns_by_rfc724_mid(
context: &Context,
rfc724_mid: &str,
additional_rfc724_mids: Vec<String>,
) -> Result<()> {
context
.sql
.transaction(|transaction| {
let mut stmt = transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
stmt.execute((rfc724_mid,))?;
for additional_rfc724_mid in additional_rfc724_mids {
stmt.execute((additional_rfc724_mid,))?;
}
Ok(())
})
.await
}
/// Tries to send MDN for message identified by `rfc724_mdn` to `contact_id`.
///
/// Attempts to aggregate additional MDNs for `contact_id` into sent MDN.
@@ -706,30 +687,28 @@ async fn send_mdn_rfc724_mid(
)
.await?;
let queued_mdn = mimefactory::mdn(
let mimefactory = MimeFactory::from_mdn(
context,
contact_id,
rfc724_mid,
rfc724_mid.to_string(),
additional_rfc724_mids.clone(),
)
.await?;
let bcc_self = queued_mdn.bcc_self;
let encrypted = queued_mdn.encryption.is_encrypted();
let mut recipients = queued_mdn.recipients.clone();
let public_key = key::load_self_public_key(context).await?;
let secret_key = key::load_self_secret_key(context).await?;
let encrypted = mimefactory.will_be_encrypted();
let mut recipients = if contact_id == ContactId::SELF {
Vec::new()
} else {
mimefactory.recipients()
};
let from = smtp
.from
.as_ref()
.context("No From address, not connected")?
.to_string();
let rendered_msg =
mimefactory::render_queued_mail(queued_mdn, &public_key, &secret_key, from.clone())?;
let rendered_msg = Box::pin(mimefactory.render(context, &from)).await?;
let body = rendered_msg.message;
if bcc_self {
if context.get_config_bool(Config::BccSelf).await? {
add_self_recipients(context, &mut recipients, encrypted, from).await?;
}
let recipients: Vec<_> = recipients
@@ -741,13 +720,24 @@ async fn send_mdn_rfc724_mid(
.ok()
})
.collect();
message::insert_tombstone(context, &rendered_msg.rfc724_mid, false).await?;
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
match smtp_send(context, &recipients, &body, smtp, None).await {
SendResult::Success => {
if !recipients.is_empty() {
info!(context, "Successfully sent MDN for {rfc724_mid}.");
}
delete_mdns_by_rfc724_mid(context, rfc724_mid, additional_rfc724_mids).await?;
context
.sql
.transaction(|transaction| {
let mut stmt =
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
stmt.execute((rfc724_mid,))?;
for additional_rfc724_mid in additional_rfc724_mids {
stmt.execute((additional_rfc724_mid,))?;
}
Ok(())
})
.await?;
Ok(true)
}
SendResult::Retry => {
+1 -1
View File
@@ -109,7 +109,7 @@ pub struct SideEffects {
}
/// Email message ready to be queued with the side effects that should be applied at the same time.
pub(crate) type ToBeQueuedMail = (QueuedMail, SideEffects);
pub(crate) type ToBeQueuedMail = (QueuedMail, Option<SideEffects>);
/// Process side effects and store queued mail.
pub(crate) fn enqueue_mail(
+1 -7
View File
@@ -14,7 +14,7 @@ use crate::config::Config;
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
use crate::ephemeral::start_ephemeral_timers;
use crate::imex::{self, BLOBS_BACKUP_NAME};
use crate::imex::BLOBS_BACKUP_NAME;
use crate::location;
use crate::log::{LogExt, warn};
use crate::message::MsgId;
@@ -827,12 +827,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
imex::maybe_readd_backup_transfer_msg(context)
.await
.context("Failed to re-add backup transfer message")
.log_err(context)
.ok();
if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}.");
}
+4 -3
View File
@@ -35,7 +35,7 @@ use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, self_fingerprint};
use crate::message::{Message, MessageState, MsgId};
use crate::mimefactory;
use crate::mimefactory::{self, MimeFactory};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::pgp::SeipdVersion;
use crate::receive_imf::{ReceivedMsg, receive_imf};
@@ -852,8 +852,9 @@ ORDER BY id"
/// Receives a read receipt from `reader`, who received `msg`.
pub async fn recv_mdn(&self, reader: &TestContext, msg: &Message) -> Result<()> {
let queued_mdn = mimefactory::mdn(reader, msg.from_id, &msg.rfc724_mid, vec![]).await?;
let mdn = mimefactory::render_queued_mail_with_context(queued_mdn, reader)
let mdn = MimeFactory::from_mdn(reader, msg.from_id, msg.rfc724_mid.clone(), vec![])
.await?
.render(reader, &reader.get_primary_self_addr().await?)
.await?
.message;
receive_imf(self, mdn.as_bytes(), false).await?;
-1
View File
@@ -220,7 +220,6 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
),
Some(&mut msg),
true,
time(),
)
.await
.ok();