mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
Compare commits
3 Commits
iequidoo/s
...
r10s/file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1e11a28b | ||
|
|
f239f8d2e4 | ||
|
|
00f911e030 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -23,7 +23,7 @@ env:
|
||||
RUST_VERSION: 1.95.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.89.0
|
||||
MSRV: 1.88.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
@@ -41,8 +41,6 @@ jobs:
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run clippy
|
||||
@@ -94,8 +92,6 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Rustdoc
|
||||
run: cargo doc --document-private-items --no-deps
|
||||
|
||||
@@ -139,11 +135,9 @@ jobs:
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
@@ -175,8 +169,6 @@ jobs:
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build C library
|
||||
run: cargo build -p deltachat_ffi
|
||||
@@ -203,8 +195,6 @@ jobs:
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build deltachat-rpc-server
|
||||
run: cargo build -p deltachat-rpc-server
|
||||
|
||||
14
.github/workflows/deltachat-rpc-server.yml
vendored
14
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Build deltachat-rpc-server wheels
|
||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
|
||||
- name: Download Linux aarch64 binary
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
6
.github/workflows/nix.yml
vendored
6
.github/workflows/nix.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix fmt flake.nix -- --check
|
||||
|
||||
build:
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
build-macos:
|
||||
@@ -105,5 +105,5 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- run: nix build .#${{ matrix.installable }}
|
||||
|
||||
2
.github/workflows/repl.yml
vendored
2
.github/workflows/repl.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build
|
||||
run: nix build .#deltachat-repl-win64
|
||||
- name: Upload binary
|
||||
|
||||
4
.github/workflows/upload-docs.yml
vendored
4
.github/workflows/upload-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build Python documentation
|
||||
run: nix build .#python-docs
|
||||
- name: Upload to py.delta.chat
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
- name: Build C documentation
|
||||
run: nix build .#docs
|
||||
- name: Upload to c.delta.chat
|
||||
|
||||
89
Cargo.lock
generated
89
Cargo.lock
generated
@@ -36,7 +36,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -136,7 +136,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -194,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "astral-tokio-tar"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693"
|
||||
checksum = "3c23f3af104b40a3430ccb90ed5f7bd877a8dc5c26fc92fde51a22b40890dcf9"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"futures-core",
|
||||
@@ -497,16 +497,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.5"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq 0.4.2",
|
||||
"cpufeatures 0.3.0",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -799,7 +799,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -934,9 +934,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorutils-rs"
|
||||
version = "0.8.0"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69abc9a8ed011e2b7946769f460b9e76e8b659ece9ef4001b9d8bba3489f796d"
|
||||
checksum = "6e2fc25857fa523662de5cae84225b0e7bfb24a2a3f9ed8802fecf03df7252b1"
|
||||
dependencies = [
|
||||
"erydanos",
|
||||
"half",
|
||||
@@ -1011,15 +1011,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
@@ -1225,7 +1216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
@@ -1301,9 +1292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "dbl"
|
||||
@@ -2610,9 +2601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -2625,6 +2616,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -2662,7 +2654,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -3254,7 +3246,7 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3268,9 +3260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -3461,13 +3453,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3941,14 +3933,15 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.79"
|
||||
version = "0.10.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
|
||||
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
@@ -3981,9 +3974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.115"
|
||||
version = "0.9.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
|
||||
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -4419,7 +4412,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -4431,7 +4424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
@@ -5515,7 +5508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -5526,7 +5519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -5554,7 +5547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -5732,12 +5725,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6151,9 +6144,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -6161,7 +6154,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.3",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.1",
|
||||
]
|
||||
@@ -6178,9 +6171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "deltachat"
|
||||
version = "2.50.0-dev"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -53,7 +53,7 @@ blake3 = "1.8.2"
|
||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
colorutils-rs = { version = "0.8.0", default-features = false }
|
||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||
data-encoding = "2.9.0"
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "1"
|
||||
@@ -103,7 +103,7 @@ thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
astral-tokio-tar = { version = "0.6.1", default-features = false }
|
||||
astral-tokio-tar = { version = "0.6", default-features = false }
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
|
||||
13
STYLE.md
13
STYLE.md
@@ -161,16 +161,3 @@ are documented.
|
||||
|
||||
Follow Rust guidelines for the documentation comments:
|
||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||
|
||||
## Do not use `into()`, `try_into()` or `parse()`
|
||||
|
||||
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
|
||||
Instead, a `new()` function is recommended.
|
||||
|
||||
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
|
||||
over `into()`, `try_into()` or `parse()`.
|
||||
|
||||
Calling `into()`, `try_into()` or `parse()`
|
||||
creates an indirection,
|
||||
which is hard to follow for people who are not familiar with Rust,
|
||||
or who are not using rust-analyzer.
|
||||
|
||||
@@ -4003,6 +4003,8 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
|
||||
* Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
|
||||
*
|
||||
* Outgoing message states:
|
||||
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING. Deprecated.
|
||||
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
|
||||
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
|
||||
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
|
||||
@@ -5587,6 +5589,13 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_STATE_IN_SEEN 16
|
||||
|
||||
/**
|
||||
* Outgoing message being prepared. See dc_msg_get_state() for details.
|
||||
*
|
||||
* @deprecated 2024-12-07
|
||||
*/
|
||||
#define DC_STATE_OUT_PREPARING 18
|
||||
|
||||
/**
|
||||
* Outgoing message drafted. See dc_msg_get_state() for details.
|
||||
*/
|
||||
|
||||
@@ -230,6 +230,7 @@ pub enum LotState {
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
MsgInSeen = 16,
|
||||
MsgOutPreparing = 18,
|
||||
MsgOutDraft = 19,
|
||||
MsgOutPending = 20,
|
||||
MsgOutFailed = 24,
|
||||
@@ -245,6 +246,7 @@ impl From<MessageState> for LotState {
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
|
||||
@@ -1882,6 +1882,20 @@ impl CommandApi {
|
||||
deltachat::contact::make_vcard(&ctx, &contacts).await
|
||||
}
|
||||
|
||||
/// Sets vCard containing the given contacts to the message draft.
|
||||
async fn set_draft_vcard(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
contacts: Vec<u32>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
||||
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
msg.make_vcard(&ctx, &contacts).await?;
|
||||
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
@@ -2407,7 +2421,6 @@ impl CommandApi {
|
||||
chat::resend_msgs(&ctx, &message_ids).await
|
||||
}
|
||||
|
||||
/// @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead.
|
||||
async fn send_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -13,7 +13,7 @@ def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
@@ -21,30 +21,36 @@ def main():
|
||||
account.set_config("bot", "1")
|
||||
if not account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
|
||||
account.set_config("addr", sys.argv[1])
|
||||
account.set_config("mail_pw", sys.argv[2])
|
||||
account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
deltachat.start_io()
|
||||
|
||||
qr = account.get_qr_code()
|
||||
logging.info(f"Invite link: {qr}")
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.INFO:
|
||||
logging.info(event["msg"])
|
||||
elif event.kind == EventType.WARNING:
|
||||
logging.warning(event["msg"])
|
||||
elif event.kind == EventType.ERROR:
|
||||
logging.error(event["msg"])
|
||||
elif event.kind == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
message = account.get_message_by_id(event.msg_id)
|
||||
def process_messages():
|
||||
for message in account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
process_messages()
|
||||
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event["kind"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["kind"] == EventType.WARNING:
|
||||
logging.warning("%s", event["msg"])
|
||||
elif event["kind"] == EventType.ERROR:
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["kind"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .chat import Chat
|
||||
@@ -391,7 +392,8 @@ class Account:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, process "incoming message" events instead.
|
||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
@@ -461,6 +463,16 @@ class Account:
|
||||
"""Wait for reaction change event."""
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
|
||||
@@ -164,7 +164,7 @@ class Chat:
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
def send_sticker(self, path: str) -> Message:
|
||||
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
|
||||
"""Send an sticker and return the resulting Message instance."""
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ class MessageState(IntEnum):
|
||||
IN_FRESH = 10
|
||||
IN_NOTICED = 13
|
||||
IN_SEEN = 16
|
||||
OUT_PREPARING = 18
|
||||
OUT_DRAFT = 19
|
||||
OUT_PENDING = 20
|
||||
OUT_FAILED = 24
|
||||
|
||||
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState, ViewType
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError, Rpc
|
||||
|
||||
@@ -550,7 +550,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, log) -> None:
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("image/avatar64x64.png")
|
||||
chat1.send_file(str(original_image_path))
|
||||
chat1.send_message(file=str(original_image_path), viewtype=ViewType.IMAGE)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmp_path / "attachment.txt"
|
||||
|
||||
13
deny.toml
13
deny.toml
@@ -33,17 +33,7 @@ ignore = [
|
||||
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||
# which is a dependency of iroh 0.35.0.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
|
||||
"RUSTSEC-2026-0104",
|
||||
|
||||
# hickory-proto 0.25.2 unbounded loop in DNSSEC code.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0118>
|
||||
"RUSTSEC-2026-0118",
|
||||
|
||||
# hickory-proto 0.25.2 quadratic complexity issue.
|
||||
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
|
||||
"RUSTSEC-2026-0119"
|
||||
"RUSTSEC-2026-0104"
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -55,7 +45,6 @@ skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "constant_time_eq", version = "0.3.1" },
|
||||
{ name = "cpufeatures", version = "0.2.17" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
|
||||
@@ -271,6 +271,15 @@ class Chat:
|
||||
sent out. This is the same object as was passed in, which
|
||||
has been modified with the new state of the core.
|
||||
"""
|
||||
if msg.is_out_preparing():
|
||||
assert msg.id != 0
|
||||
# get a fresh copy of dc_msg, the core needs it
|
||||
maybe_msg = Message.from_db(self.account, msg.id)
|
||||
if maybe_msg is not None:
|
||||
msg = maybe_msg
|
||||
else:
|
||||
raise ValueError("message does not exist")
|
||||
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
@@ -324,6 +333,26 @@ class Chat:
|
||||
raise ValueError("message could not be sent")
|
||||
return Message.from_db(self.account, sent_id)
|
||||
|
||||
def send_prepared(self, message):
|
||||
"""send a previously prepared message.
|
||||
|
||||
:param message: a :class:`Message` instance previously returned by
|
||||
:meth:`prepare_file`.
|
||||
:raises ValueError: if message can not be sent.
|
||||
:returns: a :class:`deltachat.message.Message` instance as sent out.
|
||||
"""
|
||||
assert message.id != 0 and message.is_out_preparing()
|
||||
# get a fresh copy of dc_msg, the core needs it
|
||||
msg = Message.from_db(self.account, message.id)
|
||||
|
||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
||||
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
||||
if sent_id == 0:
|
||||
raise ValueError("message could not be sent")
|
||||
assert sent_id == msg.id
|
||||
# modify message in place to avoid bad state for the caller
|
||||
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
|
||||
|
||||
def set_draft(self, message):
|
||||
"""set message as draft.
|
||||
|
||||
|
||||
@@ -351,12 +351,17 @@ class Message:
|
||||
def is_outgoing(self):
|
||||
"""Return True if Message is outgoing."""
|
||||
return lib.dc_msg_get_state(self._dc_msg) in (
|
||||
const.DC_STATE_OUT_PREPARING,
|
||||
const.DC_STATE_OUT_PENDING,
|
||||
const.DC_STATE_OUT_FAILED,
|
||||
const.DC_STATE_OUT_MDN_RCVD,
|
||||
const.DC_STATE_OUT_DELIVERED,
|
||||
)
|
||||
|
||||
def is_out_preparing(self):
|
||||
"""Return True if Message is outgoing, but its file is being prepared."""
|
||||
return self._msgstate == const.DC_STATE_OUT_PREPARING
|
||||
|
||||
def is_out_pending(self):
|
||||
"""Return True if Message is outgoing, but is pending (no single checkmark)."""
|
||||
return self._msgstate == const.DC_STATE_OUT_PENDING
|
||||
|
||||
@@ -421,6 +421,7 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
res_viewtype: Some(Viewtype::File),
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
@@ -436,6 +437,7 @@ async fn test_recode_image_balanced_png() {
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
res_viewtype: Some(Viewtype::File),
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
set_draft: true,
|
||||
@@ -605,8 +607,10 @@ impl SendImageCheckMediaquality<'_> {
|
||||
}
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
if viewtype != Viewtype::File {
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
}
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
@@ -621,8 +625,10 @@ impl SendImageCheckMediaquality<'_> {
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
if viewtype != Viewtype::File {
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
}
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
@@ -738,6 +744,31 @@ async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that image sent with "File" viewtype keeps the viewtype
|
||||
/// and is not converted into Image viewtype because of .png extension.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_image_as_file() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
|
||||
let bytes = include_bytes!("../../test-data/image/logo.png");
|
||||
let path = alice.get_blobdir().join("logo").with_extension("png");
|
||||
fs::write(&path, bytes)
|
||||
.await
|
||||
.context("Failed to write file")?;
|
||||
|
||||
// Set image as file.
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(alice, &path, None, None)?;
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
|
||||
// Viewtype is still File, not converted to Image.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
70
src/chat.rs
70
src/chat.rs
@@ -2465,26 +2465,25 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.param
|
||||
.get_file_blob(context)?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let mut maybe_image = false;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
// formats as GIF or VOICE.
|
||||
//
|
||||
// Typical conversions:
|
||||
// - from FILE to AUDIO/VIDEO/IMAGE
|
||||
// - from FILE/IMAGE to GIF */
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
|
||||
if better_type == Viewtype::Image {
|
||||
maybe_image = true;
|
||||
} else if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
|
||||
&& (better_type == Viewtype::Video
|
||||
|| better_type == Viewtype::Audio
|
||||
|| better_type == Viewtype::Gif)
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::File {
|
||||
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
|
||||
&& (better_type == Viewtype::Vcard
|
||||
|| better_type == Viewtype::Webxdc
|
||||
&& context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok())
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
@@ -2495,7 +2494,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
if msg.viewtype == Viewtype::File && maybe_image || msg.viewtype == Viewtype::Image {
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
let new_name = blob
|
||||
.check_or_recode_image(context, msg.get_filename(), &mut msg.viewtype)
|
||||
.await?;
|
||||
@@ -2613,7 +2612,7 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
"chat_id cannot be a special chat: {chat_id}"
|
||||
);
|
||||
|
||||
if msg.state != MessageState::Undefined {
|
||||
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
// create_send_msg_jobs() will update `param` in the db.
|
||||
@@ -2721,7 +2720,10 @@ async fn prepare_send_msg(
|
||||
None
|
||||
};
|
||||
|
||||
if msg.state == MessageState::Undefined
|
||||
if matches!(
|
||||
msg.state,
|
||||
MessageState::Undefined | MessageState::OutPreparing
|
||||
)
|
||||
// Legacy SecureJoin "v*-request" messages are unencrypted.
|
||||
&& msg.param.get_cmd() != SystemMessage::SecurejoinMessage
|
||||
&& chat.is_encrypted(context).await?
|
||||
@@ -2934,8 +2936,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
-- From `InFresh` to `OutMdnRcvd` inclusive except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26,28) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
@@ -2970,6 +2972,15 @@ WHERE id=?
|
||||
)?;
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_chunk = recipients_chunk.join(" ");
|
||||
if let Some(pre_msg) = &rendered_pre_msg {
|
||||
let row_id = stmt.execute((
|
||||
&pre_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&pre_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
let row_id = stmt.execute((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
@@ -2977,16 +2988,6 @@ WHERE id=?
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
let Some(pre_msg) = &rendered_pre_msg else {
|
||||
continue;
|
||||
};
|
||||
let row_id = stmt.execute((
|
||||
&pre_msg.rfc724_mid,
|
||||
&recipients_chunk,
|
||||
&pre_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id.try_into()?);
|
||||
}
|
||||
Ok(row_ids)
|
||||
};
|
||||
@@ -4537,7 +4538,6 @@ pub async fn forward_msgs_2ctx(
|
||||
|
||||
msg.state = MessageState::OutPending;
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.pre_rfc724_mid.clear();
|
||||
msg.timestamp_sort = curr_timestamp;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
|
||||
|
||||
@@ -407,6 +407,9 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Make all outgoing messages with Autocrypt header "multipart/signed".
|
||||
SignUnencrypted,
|
||||
|
||||
/// Let the core save all events to the database.
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -707,6 +710,7 @@ impl Context {
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
| Config::SyncMsgs
|
||||
| Config::SignUnencrypted
|
||||
| Config::DisableIdle => {
|
||||
ensure!(
|
||||
matches!(value, None | Some("0") | Some("1")),
|
||||
@@ -940,18 +944,6 @@ impl Context {
|
||||
/// Determine whether the specified addr maps to the/a self addr.
|
||||
/// Returns `false` if no addresses are configured.
|
||||
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
// Employ the config cache to optimize for `ConfiguredAddr` passed.
|
||||
if !addr.is_empty()
|
||||
&& addr_cmp(
|
||||
addr,
|
||||
&self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(self
|
||||
.get_all_self_addrs()
|
||||
.await?
|
||||
|
||||
@@ -991,6 +991,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"sign_unencrypted",
|
||||
self.get_config_int(Config::SignUnencrypted)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
|
||||
10
src/e2ee.rs
10
src/e2ee.rs
@@ -79,6 +79,16 @@ impl EncryptHelper {
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
/// Signs the passed-in `mail` using the private key from `context`.
|
||||
/// Returns the payload and the signature.
|
||||
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
let mut buffer = Vec::new();
|
||||
mail.clone().write_part(&mut buffer)?;
|
||||
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a private key exists for the configured user.
|
||||
|
||||
@@ -871,7 +871,7 @@ mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[test]
|
||||
@@ -1103,9 +1103,6 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
.await?;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
// Bob needs the chat accepted so that "normal" messages from Alice trigger `IncomingMsg`.
|
||||
// Location-only messages still must trigger `MsgsChanged`.
|
||||
bob.create_chat(alice).await;
|
||||
|
||||
// Alice enables location streaming.
|
||||
// Bob receives a message saying that Alice enabled location streaming.
|
||||
@@ -1120,18 +1117,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
delete_expired(alice, time()).await?;
|
||||
maybe_send(alice).await?;
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
|
||||
bob.evtracker
|
||||
.get_matching_ex(
|
||||
bob,
|
||||
ExpectedEvents {
|
||||
expected: |e| matches!(e, EventType::MsgsChanged { .. }),
|
||||
unexpected: |e| matches!(e, EventType::IncomingMsg { .. }),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
|
||||
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ pub struct EnteredImapLoginParam {
|
||||
/// Folder to watch.
|
||||
///
|
||||
/// If empty, user has not entered anything and it shuold expand to "INBOX" later.
|
||||
#[serde(default)]
|
||||
pub folder: String,
|
||||
|
||||
/// Socket security.
|
||||
|
||||
@@ -1381,8 +1381,13 @@ pub enum MessageState {
|
||||
/// IMAP and MDN may be sent.
|
||||
InSeen = 16,
|
||||
|
||||
// Deprecated 2024-12-07. Removed 2026-04.
|
||||
// OutPreparing = 18,
|
||||
/// For files which need time to be prepared before they can be
|
||||
/// sent, the message enters this state before
|
||||
/// OutPending.
|
||||
///
|
||||
/// Deprecated 2024-12-07.
|
||||
OutPreparing = 18,
|
||||
|
||||
/// Message saved as draft.
|
||||
OutDraft = 19,
|
||||
|
||||
@@ -1415,6 +1420,7 @@ impl std::fmt::Display for MessageState {
|
||||
Self::InFresh => "Fresh",
|
||||
Self::InNoticed => "Noticed",
|
||||
Self::InSeen => "Seen",
|
||||
Self::OutPreparing => "Preparing",
|
||||
Self::OutDraft => "Draft",
|
||||
Self::OutPending => "Pending",
|
||||
Self::OutFailed => "Failed",
|
||||
@@ -1431,7 +1437,7 @@ impl MessageState {
|
||||
use MessageState::*;
|
||||
matches!(
|
||||
self,
|
||||
OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
|
||||
OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1440,7 +1446,7 @@ impl MessageState {
|
||||
use MessageState::*;
|
||||
matches!(
|
||||
self,
|
||||
OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
|
||||
OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1227,18 +1227,53 @@ impl MimeFactory {
|
||||
message.header(header, value)
|
||||
});
|
||||
let message = MimePart::new("multipart/mixed", vec![message]);
|
||||
let message = protected_headers
|
||||
let mut message = protected_headers
|
||||
.iter()
|
||||
.fold(message, |message, (header, value)| {
|
||||
message.header(*header, value.clone())
|
||||
});
|
||||
|
||||
// Deduplicate unprotected headers that also are in the protected headers:
|
||||
let protected: HashSet<&str> =
|
||||
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
|
||||
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
|
||||
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
|
||||
// Deduplicate unprotected headers that also are in the protected headers:
|
||||
let protected: HashSet<&str> =
|
||||
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
|
||||
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
|
||||
|
||||
message
|
||||
message
|
||||
} else {
|
||||
for (h, v) in &mut message.headers {
|
||||
if h == "Content-Type"
|
||||
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
|
||||
{
|
||||
let mut ct_new = ct.clone();
|
||||
ct_new = ct_new.attribute("protected-headers", "v1");
|
||||
if use_std_header_protection {
|
||||
ct_new = ct_new.attribute("hp", "clear");
|
||||
}
|
||||
*ct = ct_new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let signature = encrypt_helper.sign(context, &message).await?;
|
||||
MimePart::new(
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"; protected",
|
||||
vec![
|
||||
message,
|
||||
MimePart::new(
|
||||
"application/pgp-signature; name=\"signature.asc\"",
|
||||
signature,
|
||||
)
|
||||
.header(
|
||||
"Content-Description",
|
||||
mail_builder::headers::raw::Raw::<'static>::new(
|
||||
"OpenPGP digital signature",
|
||||
),
|
||||
)
|
||||
.attachment("signature"),
|
||||
],
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let MimeFactory {
|
||||
@@ -2157,6 +2192,10 @@ fn group_headers_by_confidentiality(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the header to the protected headers
|
||||
// in case of signed-only message.
|
||||
// If the message is not signed, this value will not be used.
|
||||
protected_headers.push(header.clone());
|
||||
unprotected_headers.push(header.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,6 +601,70 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_unencrypted_signed() {
|
||||
// create chat with bob, set selfavatar
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::SignUnencrypted, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
|
||||
|
||||
let file = t.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// send message to bob: that should get multipart/signed.
|
||||
// `Subject:` is protected by copying it.
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("multipart/signed").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 0);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
|
||||
let body = payload.next().unwrap();
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
}
|
||||
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
|
||||
@@ -304,9 +304,37 @@ impl MimeMessage {
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
let (part, mimetype) =
|
||||
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
|
||||
if let Some(part) = mail.subparts.first() {
|
||||
// We don't remove "subject" from `headers` because currently just signed
|
||||
// messages are shown as unencrypted anyway.
|
||||
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
part,
|
||||
);
|
||||
(part, part.ctype.mimetype.parse::<Mime>()?)
|
||||
} else {
|
||||
// Not a valid signed message, handle it as plaintext.
|
||||
(&mail, mimetype)
|
||||
}
|
||||
} else {
|
||||
// Currently we do not sign unencrypted messages by default.
|
||||
(&mail, mimetype)
|
||||
};
|
||||
if mimetype.type_() == mime::MULTIPART
|
||||
&& mimetype.subtype().as_str() == "mixed"
|
||||
&& let Some(part) = mail.subparts.first()
|
||||
&& let Some(part) = part.subparts.first()
|
||||
{
|
||||
for field in &part.headers {
|
||||
let key = field.get_key().to_lowercase();
|
||||
@@ -330,7 +358,8 @@ impl MimeMessage {
|
||||
);
|
||||
}
|
||||
|
||||
// Remove headers that are allowed _only_ in the encrypted+signed part
|
||||
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
|
||||
// them in signed-only emails, but has no value currently.
|
||||
let encrypted = false;
|
||||
Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
|
||||
|
||||
@@ -490,13 +519,11 @@ impl MimeMessage {
|
||||
let mail = mail.as_ref().map(|mail| {
|
||||
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
|
||||
.unwrap_or((mail, Default::default()));
|
||||
if is_encrypted {
|
||||
let signatures_detached = signatures_detached
|
||||
.into_iter()
|
||||
.map(|fp| (fp, Vec::new()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
signatures.extend(signatures_detached);
|
||||
}
|
||||
let signatures_detached = signatures_detached
|
||||
.into_iter()
|
||||
.map(|fp| (fp, Vec::new()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
signatures.extend(signatures_detached);
|
||||
content
|
||||
});
|
||||
|
||||
@@ -2190,6 +2217,9 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
/// Returns whether the outer header value must be ignored if the message contains a signed (and
|
||||
/// optionally encrypted) part. This is independent from the modern Header Protection defined in
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9788.html>.
|
||||
///
|
||||
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
|
||||
/// signed-only messages. Such messages are shown as unencrypted anyway.
|
||||
fn is_protected(key: &str) -> bool {
|
||||
key.starts_with("chat-")
|
||||
|| matches!(
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
chat,
|
||||
chatlist::Chatlist,
|
||||
constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
contact::Contact,
|
||||
key,
|
||||
message::{MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
@@ -2042,24 +2041,32 @@ async fn test_multiple_autocrypt_hdrs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests receiving a simple signed-unencrypted message
|
||||
/// that was generated by an old version of Core that supported sending such messages.
|
||||
/// Tests that timestamp of signed but not encrypted message is protected.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_signed_only() -> Result<()> {
|
||||
async fn test_protected_date() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let imf_raw = include_bytes!("../../test-data/message/unencrypted_signed_simple.eml");
|
||||
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
|
||||
assert_eq!(msg.msg_ids.len(), 1);
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.get_text(), "Hello!");
|
||||
assert_eq!(msg.viewtype, Viewtype::Text);
|
||||
assert_eq!(msg.get_timestamp(), 1615987853);
|
||||
alice.set_config(Config::SignUnencrypted, Some("1")).await?;
|
||||
|
||||
let alice_contact = Contact::get_by_id(bob, msg.from_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
let alice_msg_id = chat::send_text_msg(alice, alice_chat.id, "Hello!".to_string()).await?;
|
||||
let alice_msg = Message::load_from_db(alice, alice_msg_id).await?;
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
|
||||
let mut sent_msg = alice.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg.payload.replacen(
|
||||
"Date:",
|
||||
"Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)\r\nX-Not-Date:",
|
||||
1,
|
||||
);
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(alice_msg.get_text(), bob_msg.get_text());
|
||||
|
||||
// Timestamp that the sender has put into the message
|
||||
// should always be displayed as is on the receiver.
|
||||
assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
49
src/pgp.rs
49
src/pgp.rs
@@ -6,15 +6,15 @@ use std::io::Cursor;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::composed::{
|
||||
Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType, MessageBuilder,
|
||||
SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
||||
SubkeyParamsBuilder, SubpacketConfig,
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{Signature, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
@@ -202,6 +202,47 @@ pub async fn pk_encrypt(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Produces a detached signature for `plain` text using `private_key_for_signing`.
|
||||
pub fn pk_calc_signature(
|
||||
plain: Vec<u8>,
|
||||
private_key_for_signing: &SignedSecretKey,
|
||||
) -> Result<String> {
|
||||
let rng = thread_rng();
|
||||
|
||||
let mut config = SignatureConfig::from_key(
|
||||
rng,
|
||||
&private_key_for_signing.primary_key,
|
||||
SignatureType::Binary,
|
||||
)?;
|
||||
|
||||
config.hashed_subpackets = vec![
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
private_key_for_signing.fingerprint(),
|
||||
))?,
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
pgp::types::Timestamp::now(),
|
||||
))?,
|
||||
];
|
||||
config.unhashed_subpackets = vec![];
|
||||
if private_key_for_signing.version() <= KeyVersion::V4 {
|
||||
config
|
||||
.unhashed_subpackets
|
||||
.push(Subpacket::regular(SubpacketData::IssuerKeyId(
|
||||
private_key_for_signing.legacy_key_id(),
|
||||
))?);
|
||||
}
|
||||
|
||||
let signature = config.sign(
|
||||
&private_key_for_signing.primary_key,
|
||||
&Password::empty(),
|
||||
plain.as_slice(),
|
||||
)?;
|
||||
|
||||
let sig = DetachedSignature::new(signature);
|
||||
|
||||
Ok(sig.to_armored_string(ArmorOptions::default())?)
|
||||
}
|
||||
|
||||
/// Returns fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures in `msg` and corresponding intended recipient fingerprints
|
||||
|
||||
@@ -66,6 +66,12 @@ impl Context {
|
||||
|
||||
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
||||
/// and emits an event to let the UIs update connectivity view.
|
||||
///
|
||||
/// Moreover, once each time quota gets larger than `QUOTA_WARN_THRESHOLD_PERCENTAGE`,
|
||||
/// a device message is added.
|
||||
/// As the message is added only once, the user is not spammed
|
||||
/// in case for some providers the quota is always at ~100%
|
||||
/// and new space is allocated as needed.
|
||||
pub(crate) async fn update_recent_quota(
|
||||
&self,
|
||||
session: &mut ImapSession,
|
||||
|
||||
@@ -525,10 +525,18 @@ pub(crate) async fn receive_imf_inner(
|
||||
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
|
||||
);
|
||||
|
||||
let msg_id = message::rfc724_mid_exists(context, rfc724_mid_orig).await?;
|
||||
if let Some(msg_id) = msg_id
|
||||
&& !mime_parser.incoming
|
||||
{
|
||||
// These checks must be done before processing of SecureJoin and other special messages.
|
||||
if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
|
||||
// Post-Message just replaces the attachment and modifies Params, not the whole message.
|
||||
// This is done in the `handle_post_message` method.
|
||||
} else if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await? {
|
||||
info!(
|
||||
context,
|
||||
"Message {rfc724_mid} is already in some chat or deleted."
|
||||
);
|
||||
if mime_parser.incoming {
|
||||
return Ok(None);
|
||||
}
|
||||
// For the case if we missed a successful SMTP response. Be optimistic that the message is
|
||||
// delivered also.
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
@@ -543,16 +551,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
}
|
||||
// These checks must be done before processing of SecureJoin and other special messages.
|
||||
if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
|
||||
// Post-Message just replaces the attachment and modifies Params, not the whole message.
|
||||
// This is done in the `handle_post_message` method.
|
||||
} else if msg_id.is_some() {
|
||||
info!(
|
||||
context,
|
||||
"Message {rfc724_mid} is already in some chat or deleted."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -808,6 +806,8 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
if transport_changed {
|
||||
info!(context, "Primary transport changed to {from_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
|
||||
// Regenerate User ID in V4 keys.
|
||||
context.self_public_key.lock().await.take();
|
||||
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
@@ -1019,15 +1019,8 @@ UPDATE msgs SET state=? WHERE
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
|
||||
let skip_bot_notify = is_bot && is_pre_message;
|
||||
let is_empty = !is_pre_message
|
||||
&& mime_parser.parts.first().is_none_or(|p| {
|
||||
p.typ == Viewtype::Text && p.msg.is_empty() && p.param.get(Param::Quote).is_none()
|
||||
});
|
||||
let important = mime_parser.incoming
|
||||
&& !is_empty
|
||||
&& fresh
|
||||
&& !is_old_contact_request
|
||||
&& !skip_bot_notify;
|
||||
let important =
|
||||
mime_parser.incoming && fresh && !is_old_contact_request && !skip_bot_notify;
|
||||
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
chat_id.emit_msg_event(context, *msg_id, important);
|
||||
@@ -2506,10 +2499,7 @@ WHERE id=?
|
||||
part.typ,
|
||||
part.bytes as isize,
|
||||
part.error.as_deref().unwrap_or_default(),
|
||||
match mime_parser.incoming {
|
||||
true => state,
|
||||
false => MessageState::Undefined,
|
||||
},
|
||||
state,
|
||||
DownloadState::Done as u32,
|
||||
original_msg.id,
|
||||
),
|
||||
|
||||
@@ -5592,27 +5592,27 @@ async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (post_msg_id, post_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, post_msg_id);
|
||||
assert!(post_msg_payload.len() > file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own post-message because of bcc_self
|
||||
// This should not yet mark the message as delivered,
|
||||
// because not everything was sent,
|
||||
// but it does remove the post-message from the SMTP queue.
|
||||
receive_imf(alice, post_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
|
||||
let (pre_msg_id, pre_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, pre_msg_id);
|
||||
assert!(pre_msg_payload.len() < file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own pre-message because of bcc_self
|
||||
// This should not yet mark the message as delivered,
|
||||
// because not everything was sent,
|
||||
// but it does remove the pre-message from the SMTP queue
|
||||
receive_imf(alice, pre_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
|
||||
let (post_msg_id, post_msg_payload) = first_row_in_smtp_queue(alice).await;
|
||||
assert_eq!(msg_id, post_msg_id);
|
||||
assert!(post_msg_payload.len() > file_bytes.len());
|
||||
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutPending);
|
||||
// Alice receives her own post-message because of bcc_self
|
||||
// This should now mark the message as delivered,
|
||||
// because everything was sent by now.
|
||||
receive_imf(alice, pre_msg_payload.as_bytes(), false).await?;
|
||||
receive_imf(alice, post_msg_payload.as_bytes(), false).await?;
|
||||
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -379,7 +379,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
if retries > 6 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("Failed to remove message with exceeded retry limit from smtp table")?;
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
|
||||
@@ -2373,27 +2373,6 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 152)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"
|
||||
UPDATE msgs SET state=26 WHERE state=28; -- Change OutMdnRcvd to OutDelivered.
|
||||
UPDATE msgs SET state=19 WHERE state=24; -- Change OutPreparing to OutFailed.
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 153)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"CREATE INDEX smtp_index_msg_id ON smtp (msg_id, id)",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -722,14 +722,12 @@ ORDER BY id"
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `SentMessage` instances representing `smtp` rows for the given message. Returned
|
||||
/// items go in reverse order for historical reasons.
|
||||
pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec<SentMessage<'a>> {
|
||||
let sent_msgs = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=? ORDER BY id DESC",
|
||||
"SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?",
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let _id: MsgId = row.get(0)?;
|
||||
@@ -1057,17 +1055,9 @@ ORDER BY id"
|
||||
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
|
||||
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
|
||||
/// the message.
|
||||
///
|
||||
/// Removes SMTP jobs existed before and marks the corresponding messages as delivered, as
|
||||
/// tracking of these jobs is probably already lost by the test code.
|
||||
pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage<'_> {
|
||||
while self.pop_sent_msg_opt(Duration::ZERO).await.is_some() {}
|
||||
let msg_id = chat::send_msg(self, chat_id, msg).await.unwrap();
|
||||
let rev_order = false;
|
||||
let res = self
|
||||
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||
.await
|
||||
.unwrap();
|
||||
let res = self.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
res.sender_msg_id, msg_id,
|
||||
"Apparently the message was not actually sent out"
|
||||
@@ -1441,12 +1431,6 @@ pub fn fiona_keypair() -> SignedSecretKey {
|
||||
#[derive(Debug)]
|
||||
pub struct EventTracker(EventEmitter);
|
||||
|
||||
/// See [`super::EventTracker::get_matching_ex`].
|
||||
pub struct ExpectedEvents<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool> {
|
||||
pub expected: E,
|
||||
pub unexpected: U,
|
||||
}
|
||||
|
||||
impl Deref for EventTracker {
|
||||
type Target = EventEmitter;
|
||||
|
||||
@@ -1483,39 +1467,21 @@ impl EventTracker {
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes all emitted events returning the first matching one if any.
|
||||
/// Consumes emitted events returning the first matching one if any.
|
||||
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
event_matcher: F,
|
||||
) -> Option<EventType> {
|
||||
self.get_matching_ex(
|
||||
ctx,
|
||||
ExpectedEvents {
|
||||
expected: event_matcher,
|
||||
unexpected: |_| false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Consumes all emitted events returning the first matching one if any. Panics on unexpected
|
||||
/// events.
|
||||
pub async fn get_matching_ex<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
args: ExpectedEvents<E, U>,
|
||||
) -> Option<EventType> {
|
||||
ctx.emit_event(EventType::Test);
|
||||
let mut found_event = None;
|
||||
loop {
|
||||
let event = self.recv().await.unwrap();
|
||||
assert!(!(args.unexpected)(&event.typ));
|
||||
if let EventType::Test = event.typ {
|
||||
return found_event;
|
||||
}
|
||||
if (args.expected)(&event.typ) {
|
||||
found_event.get_or_insert(event.typ);
|
||||
if event_matcher(&event.typ) {
|
||||
found_event = Some(event.typ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
//! Tests about forwarding and saving Pre-Messages
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -10,7 +8,6 @@ use crate::chatlist::get_last_message_for_chat;
|
||||
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tests::pre_messages::util::send_large_file_message;
|
||||
|
||||
/// Test that forwarding Pre-Message should forward additional text to not be empty
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -89,43 +86,6 @@ async fn test_forwarding_pre_message_empty_text() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_both() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice.create_group_with_members("", &[bob]).await;
|
||||
|
||||
let (pre_message, post_message, alice_msg_id) =
|
||||
send_large_file_message(alice, alice_chat_id, Viewtype::File, &vec![0u8; 200_000]).await?;
|
||||
|
||||
let msg = bob.recv_msg(&pre_message).await;
|
||||
let _ = bob.recv_msg_trash(&post_message).await;
|
||||
let msg = Message::load_from_db(bob, msg.id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.text, "test".to_owned());
|
||||
|
||||
forward_msgs(alice, &[alice_msg_id], alice_chat_id).await?;
|
||||
let rev_order = true;
|
||||
let msg = bob
|
||||
.recv_msg(
|
||||
&alice
|
||||
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.is_forwarded(), true);
|
||||
assert_eq!(msg.text, "test".to_owned());
|
||||
let _ = bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
let msg = Message::load_from_db(bob, msg.id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.is_forwarded(), true);
|
||||
assert_eq!(msg.text, "test".to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that forwarding Pre-Message should forward additional text to not be empty
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_saving_pre_message_empty_text() -> Result<()> {
|
||||
|
||||
@@ -547,8 +547,8 @@ async fn test_webxdc_updates_in_post_message_after_pre_message() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
let post_message = alice.pop_sent_msg().await;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
|
||||
let bob_instance = bob.recv_msg(&pre_message).await;
|
||||
assert_eq!(bob_instance.download_state, DownloadState::Available);
|
||||
@@ -588,8 +588,8 @@ async fn test_webxdc_updates_in_post_message_without_pre_message() -> Result<()>
|
||||
.await?;
|
||||
|
||||
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
let post_message = alice.pop_sent_msg().await;
|
||||
let pre_message = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives post-message first.
|
||||
let bob_instance = bob.recv_msg(&post_message).await;
|
||||
|
||||
@@ -791,18 +791,7 @@ pub(crate) async fn sync_transports(
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let configured_addr = transaction.query_row(
|
||||
"SELECT value FROM config WHERE keyname='configured_addr'",
|
||||
(),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
for RemovedTransportData { addr, timestamp } in removed_transports {
|
||||
if *addr == configured_addr {
|
||||
continue;
|
||||
}
|
||||
modified |= transaction.execute(
|
||||
"DELETE FROM transports
|
||||
WHERE addr=? AND add_timestamp<=?",
|
||||
|
||||
@@ -550,7 +550,7 @@ impl Context {
|
||||
|
||||
let send_now = !matches!(
|
||||
instance.state,
|
||||
MessageState::Undefined | MessageState::OutDraft
|
||||
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
|
||||
);
|
||||
|
||||
status_update.uid = Some(create_id());
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
Content-Type: multipart/signed; protocol="application/pgp-signature"; protected;
|
||||
boundary="18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0"
|
||||
MIME-Version: 1.0
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)
|
||||
X-Not-Date: Tue, 28 Apr 2026 20:20:34 +0000
|
||||
Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
|
||||
References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL
|
||||
DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM
|
||||
Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+
|
||||
+XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
|
||||
|
||||
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0
|
||||
Content-Type: multipart/mixed; protected-headers="v1"; hp="clear";
|
||||
boundary="18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0"
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>
|
||||
Subject: Message from alice@example.org
|
||||
Date: Tue, 28 Apr 2026 20:20:34 +0000
|
||||
References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
|
||||
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL
|
||||
DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM
|
||||
Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL
|
||||
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+
|
||||
+XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr
|
||||
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
|
||||
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
|
||||
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
|
||||
3jsMAg==
|
||||
|
||||
|
||||
--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello!
|
||||
--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0--
|
||||
|
||||
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0
|
||||
Content-Type: application/pgp-signature; name="signature.asc";
|
||||
charset="utf-8"
|
||||
Content-Description: OpenPGP digital signature
|
||||
Content-Disposition: attachment; filename="signature"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----=0A=0AwnUEABYIAB0WIQQub6LLI7Uy1yhjS1hksI9hqe2UQ=
|
||||
wWCafEWkQAKCRBksI9hqe2U=0AQ4qaAQCFSLVDANIjaXswP8V5zIwUSvGnUwsMD+ruozO0mG2AqA=
|
||||
D9EqpWeD6cc+is=0Av9/nvp6uHi35pUmDX0s1XKu3xbSTWg8=3D=0A=3Dr9hO=0A-----END PGP=
|
||||
SIGNATURE-----=0A
|
||||
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0--
|
||||
|
||||
|
||||
Reference in New Issue
Block a user