Compare commits

..

19 Commits

Author SHA1 Message Date
Nico de Haen
219dbd8c42 fix: use correct dir converting plaintext to HTML (#8248)
It turned out for desktop the style unicode-bidi: plaintext is needed.
dir="auto" didn't work. The style only without dir attribute worked too
but maybe it's better to have both. With this change
https://github.com/deltachat/deltachat-desktop/issues/6345 is also
solved for long messages that have a "Show full message" dialog
2026-05-15 23:11:33 +02:00
WofWca
6e70fbf824 refactor: remove outdated comment
This function doesn't return `bool` since
416131b4a2.
2026-05-15 12:09:01 +04:00
link2xt
7a39a0c8ff ci: set cache-bin to "false" for swatinem/rust-cache action
Workaround for <https://github.com/actions/runner-images/issues/14099>.
Not clear why caching ~/.cargo/bin is needed in the first place,
so we can disable it permanently.
2026-05-15 01:14:26 +00:00
link2xt
4eda257cfa ci: do not store Rust cache from PRs in jsonrpc.yml workflow
Follow-up to d75c05e717
2026-05-15 01:14:26 +00:00
link2xt
354edb042b feat: remove workaround for old filtermail
Filtermail fix <https://github.com/chatmail/relay/pull/497>
was merged more than a year ago.
2026-05-14 17:00:23 +00:00
Hocuri
4bdc3c29ed docs: Update README.md: Use ci-chatmail instead of nine (#8238)
IIRC, using nine.testrun.org for testing is bad, and ci-chatmail should
be used instead?
2026-05-13 21:50:45 +02:00
link2xt
439216c12c feat: log all connection attempt errors instead of the first one
We log all connection attempts errors as they fail already,
but once all attempts are exhausted, we only log the first error
without specifying which address failed.

The first error is frequently the least interesting
"Network is unreachable (os error 101)" that happens
when trying to connect to IPv6 address from 
a network that does not support IPv6.

To make reading the logs easier,
log all errors together with the addresses
again once all connection attempts are exhausted.
Then it will be visible that IPv6 failed
with "Network is unreachable (os error 101)"
and IPv4 failed with "Connection timeout: deadline has elapsed"
or similar error.

Before the change error looked like this:

    IMAP failed to connect to example.org:143:starttls: Connection failure: Network is unreachable (os error 101).

With the change the error looks like this:

    IMAP failed to connect to example.org:143:starttls: All connection attempts failed: Connection to [***::1]:143 failed: Network is unreachable (os error 101); Connection to [***::2]:143 failed: Network is unreachable (os error 101); Connection to x.x.x.1:143 timed out: deadline has elapsed; Connection to x.x.x.2:143 timed out: deadline has elapsed; Connection to x.x.x.3:143 timed out: deadline has elapsed.
2026-05-13 18:38:18 +00:00
link2xt
0bb4c3d073 feat: enable draft-pqc feature on pgp crate
This is needed to have support of v6 PQC keys
by the time users start generating profiles
using such keys.

Test key was generated with rsop/v0.10.0-16-gd98265f
(commit d98265f821e7bb181d06da1d634c5c4668d89e83)
using the command
cargo run --features draft-pqc generate-key \
          --profile draft-ietf-openpgp-pqc-14-v6-ed25519-mlkem768x25519
2026-05-12 21:56:05 +00:00
link2xt
64f65886f6 chore: update zerocopy from 0.7.32 to 0.7.35
Otherwise dependency resolver fails
if you enable "draft-pqc" feature on "pgp" crate.
2026-05-12 21:56:05 +00:00
link2xt
8658f938e5 test: set email addresses explicitly for the test accounts
v6 keys will not have User IDs, so email addresses
cannot be extracted from there.
2026-05-12 21:56:05 +00:00
link2xt
f75b4abefe feat: remove Content-Description and Content-Disposition from multipart/encrypted parts
This is not required by <https://datatracker.ietf.org/doc/html/rfc3156>.

Looks like Content-Description and Content-Disposition are
the same as what Thunderbird produces,
e.g. in `test-data/message/thunderbird_encrypted_signed.eml`,
and the same values also made it into Autocrypt spec.

Content-Description is likely not used by anyone.

For Content-Disposition specification see <https://datatracker.ietf.org/doc/html/rfc2183>.
While it is explicitly allowed to set the filename for inline attachment,
it does not look useful to specify generic "encrypted.asc" explicitly.
Content-Disposition for the second part results in some webmail clients
showing the second part as an attachment with "encrypted.asc" filename.
They will likely show it differently after this change,
e.g. with a different filename or actually inline.
2026-05-12 21:32:30 +00:00
link2xt
f2f2dd42ed chore: clippy::useless-borrows-in-formatting fixes 2026-05-12 21:28:45 +00:00
link2xt
8132f32e91 chore: allow passing arguments to scripts/clippy.sh
This makes it possible to run `scripts/clippy.sh --fix --allow-dirty`
to fix clippy warnings automatically.
2026-05-12 16:28:22 +00:00
dependabot[bot]
3d28951885 chore(deps): bump cachix/install-nix-action from 31.10.5 to 31.10.6
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.10.5 to 31.10.6.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](ab739621df...8aa03977d8)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.10.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 16:27:52 +00:00
dependabot[bot]
bc2d624a80 chore(deps): bump taiki-e/install-action from 2.75.19 to 2.77.1
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.19 to 2.77.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](5f57d6cb7c...cca35edeb1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.77.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 16:27:21 +00:00
link2xt
fa8fcaaa2b refactor: use some more let..else 2026-05-12 16:26:38 +00:00
link2xt
31c74d82bd fix: fix migration 152
It was converting OutFailed to OutDraft
instead of converting OutPreparing to OutFailed.
2026-05-11 20:53:00 +00:00
link2xt
6fb2f27831 Revert "fix: set dir to "auto" in body tag when converting plain-text to HTML (#8227)"
This reverts commit 58a09df49a
which was merged with failing CI.
2026-05-08 22:11:33 +02:00
adb
58a09df49a fix: set dir to "auto" in body tag when converting plain-text to HTML (#8227)
close #8223
2026-05-08 20:20:00 +02:00
44 changed files with 372 additions and 411 deletions

View File

@@ -43,6 +43,7 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Run rustfmt - name: Run rustfmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Run clippy - name: Run clippy
@@ -96,6 +97,7 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Rustdoc - name: Rustdoc
run: cargo doc --document-private-items --no-deps run: cargo doc --document-private-items --no-deps
@@ -141,9 +143,10 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Install nextest - name: Install nextest
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 uses: taiki-e/install-action@cca35edeb1d01366c2843b68fc3ca441446d73d3
with: with:
tool: nextest tool: nextest
@@ -177,6 +180,7 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Build C library - name: Build C library
run: cargo build -p deltachat_ffi run: cargo build -p deltachat_ffi
@@ -205,6 +209,7 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: Build deltachat-rpc-server - name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server run: cargo build -p deltachat-rpc-server

View File

@@ -34,7 +34,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -82,7 +82,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }} run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -106,7 +106,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -157,7 +157,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server binaries - name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -181,7 +181,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build deltachat-rpc-server wheels - name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -208,7 +208,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Download Linux aarch64 binary - name: Download Linux aarch64 binary
uses: actions/download-artifact@v7 uses: actions/download-artifact@v7

View File

@@ -25,7 +25,10 @@ jobs:
with: with:
node-version: 18.x node-version: 18.x
- name: Add Rust cache - name: Add Rust cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: npm install - name: npm install
working-directory: deltachat-jsonrpc/typescript working-directory: deltachat-jsonrpc/typescript
run: npm install run: npm install

View File

@@ -25,7 +25,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix fmt flake.nix -- --check - run: nix fmt flake.nix -- --check
build: build:
@@ -84,7 +84,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }} - run: nix build .#${{ matrix.installable }}
build-macos: build-macos:
@@ -105,5 +105,5 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- run: nix build .#${{ matrix.installable }} - run: nix build .#${{ matrix.installable }}

View File

@@ -18,7 +18,7 @@ jobs:
with: with:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build - name: Build
run: nix build .#deltachat-repl-win64 run: nix build .#deltachat-repl-win64
- name: Upload binary - name: Upload binary

View File

@@ -41,7 +41,7 @@ jobs:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number. fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build Python documentation - name: Build Python documentation
run: nix build .#python-docs run: nix build .#python-docs
- name: Upload to py.delta.chat - name: Upload to py.delta.chat
@@ -63,7 +63,7 @@ jobs:
show-progress: false show-progress: false
persist-credentials: false persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number. fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6
- name: Build C documentation - name: Build C documentation
run: nix build .#docs run: nix build .#docs
- name: Upload to c.delta.chat - name: Upload to c.delta.chat

88
Cargo.lock generated
View File

@@ -2608,6 +2608,25 @@ dependencies = [
"libm", "libm",
] ]
[[package]]
name = "hybrid-array"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9"
dependencies = [
"typenum",
]
[[package]]
name = "hybrid-array"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af"
dependencies = [
"typenum",
"zeroize",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.9.0" version = "1.9.0"
@@ -3257,6 +3276,16 @@ dependencies = [
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
] ]
[[package]]
name = "kem"
version = "0.3.0-pre.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f"
dependencies = [
"rand_core 0.6.4",
"zeroize",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -3470,6 +3499,35 @@ dependencies = [
"windows-sys 0.61.1", "windows-sys 0.61.1",
] ]
[[package]]
name = "ml-dsa"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db"
dependencies = [
"const-oid",
"hybrid-array 0.3.1",
"num-traits",
"pkcs8",
"rand_core 0.6.4",
"sha3",
"signature",
"zeroize",
]
[[package]]
name = "ml-kem"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f"
dependencies = [
"hybrid-array 0.2.3",
"kem",
"rand_core 0.6.4",
"sha3",
"zeroize",
]
[[package]] [[package]]
name = "moka" name = "moka"
version = "0.12.10" version = "0.12.10"
@@ -4205,6 +4263,8 @@ dependencies = [
"k256", "k256",
"log", "log",
"md-5", "md-5",
"ml-dsa",
"ml-kem",
"nom 8.0.0", "nom 8.0.0",
"num-bigint-dig", "num-bigint-dig",
"num-traits", "num-traits",
@@ -4223,6 +4283,7 @@ dependencies = [
"sha2", "sha2",
"sha3", "sha3",
"signature", "signature",
"slh-dsa",
"smallvec", "smallvec",
"snafu", "snafu",
"twofish", "twofish",
@@ -5687,6 +5748,25 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slh-dsa"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267"
dependencies = [
"const-oid",
"digest",
"hmac",
"hybrid-array 0.3.1",
"pkcs8",
"rand_core 0.6.4",
"sha2",
"sha3",
"signature",
"typenum",
"zerocopy",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -7425,9 +7505,9 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.32" version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"zerocopy-derive", "zerocopy-derive",
@@ -7435,9 +7515,9 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.32" version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true } num-traits = { workspace = true }
parking_lot = "0.12.4" parking_lot = "0.12.4"
percent-encoding = "2.3" percent-encoding = "2.3"
pgp = { version = "0.19.0", default-features = false } pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
pin-project = "1" pin-project = "1"
qrcodegen = "1.7.0" qrcodegen = "1.7.0"
quick-xml = { version = "0.39", features = ["escape-html"] } quick-xml = { version = "0.39", features = ["escape-html"] }

View File

@@ -275,7 +275,7 @@ pub unsafe extern "C" fn dc_get_config(
.strdup() .strdup()
} else { } else {
match config::Config::from_str(&key) match config::Config::from_str(&key)
.with_context(|| format!("Invalid key {:?}", &key)) .with_context(|| format!("Invalid key {key:?}"))
.log_err(ctx) .log_err(ctx)
{ {
Ok(key) => ctx Ok(key) => ctx

View File

@@ -122,7 +122,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
let name_f = entry.file_name(); let name_f = entry.file_name();
let name = name_f.to_string_lossy(); let name = name_f.to_string_lossy();
if name.ends_with(".eml") { if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name); let path_plus_name = format!("{real_spec}/{name}");
println!("Import: {path_plus_name}"); println!("Import: {path_plus_name}");
if poke_eml_file(context, Path::new(&path_plus_name)) if poke_eml_file(context, Path::new(&path_plus_name))
.await .await
@@ -133,11 +133,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} }
} }
} else { } else {
eprintln!("Import: Cannot open directory \"{}\".", &real_spec); eprintln!("Import: Cannot open directory {real_spec:?}.");
return false; return false;
} }
} }
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); println!("Import: {read_cnt} items read from {real_spec:?}.");
if read_cnt > 0 { if read_cnt > 0 {
context.emit_msgs_changed_without_ids(); context.emit_msgs_changed_without_ids();
} }
@@ -179,7 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
msg.get_id(), msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" }, if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" }, if msg.has_location() { "📍" } else { "" },
&contact_name, contact_name,
contact_id, contact_id,
msgtext, msgtext,
if msg.has_html() { "[HAS-HTML]" } else { "" }, if msg.has_html() { "[HAS-HTML]" } else { "" },
@@ -221,7 +221,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
}, },
statestr, statestr,
downloadstate, downloadstate,
&temp2, temp2,
); );
} }
@@ -561,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.map_or_else(String::new, |prefix| format!("{prefix}: ")), .map_or_else(String::new, |prefix| format!("{prefix}: ")),
summary.text, summary.text,
statestr, statestr,
&timestr, timestr,
if chat.is_sending_locations() { if chat.is_sending_locations() {
"📍" "📍"
} else { } else {

View File

@@ -432,7 +432,7 @@ async fn handle_cmd(
{ {
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}"); println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
} else { } else {
println!("OAuth2 not available for {}.", &addr); println!("OAuth2 not available for {addr}.");
} }
} else { } else {
println!("oauth2: set addr first."); println!("oauth2: set addr first.");

View File

@@ -29,7 +29,7 @@ $ pip install .
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. 1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Install tox `pip install -U tox` 2. Install tox `pip install -U tox`
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`. 3. Run `CHATMAIL_DOMAIN=ci-chatmail.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output. Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.

View File

@@ -43,7 +43,12 @@ ignore = [
# hickory-proto 0.25.2 quadratic complexity issue. # hickory-proto 0.25.2 quadratic complexity issue.
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02. # Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
# <https://rustsec.org/advisories/RUSTSEC-2026-0119> # <https://rustsec.org/advisories/RUSTSEC-2026-0119>
"RUSTSEC-2026-0119" "RUSTSEC-2026-0119",
# Timing side channel in ml-dsa dependency of rPGP.
# We enable PQC for encryption rather than signatures.
# <https://rustsec.org/advisories/RUSTSEC-2025-0144>
"RUSTSEC-2025-0144",
] ]
[bans] [bans]
@@ -62,6 +67,7 @@ skip = [
{ name = "getrandom", version = "0.2.12" }, { name = "getrandom", version = "0.2.12" },
{ name = "heck", version = "0.4.1" }, { name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" }, { name = "http", version = "0.2.12" },
{ name = "hybrid-array", version = "0.2.3" },
{ name = "linux-raw-sys", version = "0.4.14" }, { name = "linux-raw-sys", version = "0.4.14" },
{ name = "lru", version = "0.12.5" }, { name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" }, { name = "netlink-packet-route", version = "0.17.1" },

View File

@@ -1,3 +1,9 @@
#!/bin/sh #!/bin/sh
# Run clippy for all Rust code in the project. # Run clippy for all Rust code in the project.
cargo clippy --workspace --all-targets --all-features -- -D warnings #
# To check, run
# scripts/clippy.sh
#
# To automatically fix warnings, run
# scripts/clippy.sh --fix --allow-dirty
cargo clippy --workspace --all-targets --all-features "$@" -- -D warnings

View File

@@ -794,7 +794,7 @@ impl Config {
.with_push_subscriber(push_subscriber.clone()) .with_push_subscriber(push_subscriber.clone())
.build() .build()
.await .await
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?; .with_context(|| format!("failed to create context from file {dbfile:?}"))?;
// Try to open without a passphrase, // Try to open without a passphrase,
// but do not return an error if account is passphare-protected. // but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?; ctx.open("".to_string()).await?;

View File

@@ -2529,7 +2529,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
// running numbers, etc. // running numbers, etc.
let filename: String = match viewtype_orig { let filename: String = match viewtype_orig {
Viewtype::Voice => format!( Viewtype::Voice => format!(
"voice-messsage_{}.{}", "voice-messsage_{}.{suffix}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2537,10 +2537,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string() |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
), ),
&suffix
), ),
Viewtype::Image | Viewtype::Gif => format!( Viewtype::Image | Viewtype::Gif => format!(
"image_{}.{}", "image_{}.{suffix}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2548,10 +2547,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(), |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string(),
), ),
&suffix,
), ),
Viewtype::Video => format!( Viewtype::Video => format!(
"video_{}.{}", "video_{}.{suffix}",
chrono::Utc chrono::Utc
.timestamp_opt(msg.timestamp_sort, 0) .timestamp_opt(msg.timestamp_sort, 0)
.single() .single()
@@ -2559,7 +2557,6 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|| "YY-mm-dd_hh:mm:ss".to_string(), || "YY-mm-dd_hh:mm:ss".to_string(),
|ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string() |ts| ts.format("%Y-%m-%d_%H-%M-%S").to_string()
), ),
&suffix
), ),
_ => filename, _ => filename,
}; };
@@ -2954,6 +2951,7 @@ WHERE id=?
) )
.await?; .await?;
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| { let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new(); let mut row_ids = Vec::<i64>::new();
@@ -2967,12 +2965,12 @@ WHERE id=?
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)", VALUES (?1, ?2, ?3, ?4)",
)?; )?;
if !recipients.is_empty() { for recipients_chunk in recipients.chunks(chunk_size) {
let all_recipients = recipients.join(" "); let recipients_chunk = recipients_chunk.join(" ");
if let Some(pre_msg) = &rendered_pre_msg { if let Some(pre_msg) = &rendered_pre_msg {
let row_id = stmt.execute(( let row_id = stmt.execute((
&pre_msg.rfc724_mid, &pre_msg.rfc724_mid,
&all_recipients, &recipients_chunk,
&pre_msg.message, &pre_msg.message,
msg.id, msg.id,
))?; ))?;
@@ -2980,7 +2978,7 @@ WHERE id=?
} }
let row_id = stmt.execute(( let row_id = stmt.execute((
&rendered_msg.rfc724_mid, &rendered_msg.rfc724_mid,
&all_recipients, &recipients_chunk,
&rendered_msg.message, &rendered_msg.message,
msg.id, msg.id,
))?; ))?;

View File

@@ -707,8 +707,7 @@ async fn get_autoconfig(
ctx, ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
&format!( &format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}", "https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
&param_domain, &param_addr_urlencoded
), ),
&param.addr, &param.addr,
) )
@@ -721,7 +720,7 @@ async fn get_autoconfig(
// Outlook uses always SSL but different domains (this comment describes the next two steps) // Outlook uses always SSL but different domains (this comment describes the next two steps)
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!("https://{}/autodiscover/autodiscover.xml", &param_domain), format!("https://{param_domain}/autodiscover/autodiscover.xml"),
) )
.await .await
{ {
@@ -731,10 +730,7 @@ async fn get_autoconfig(
if let Ok(res) = outlk_autodiscover( if let Ok(res) = outlk_autodiscover(
ctx, ctx,
format!( format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
"https://autodiscover.{}/autodiscover/autodiscover.xml",
&param_domain
),
) )
.await .await
{ {
@@ -745,7 +741,7 @@ async fn get_autoconfig(
// always SSL for Thunderbird's database // always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure( if let Ok(res) = moz_autoconfigure(
ctx, ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain), &format!("https://autoconfig.thunderbird.net/v1.1/{param_domain}"),
&param.addr, &param.addr,
) )
.await .await

View File

@@ -204,6 +204,9 @@ pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// `max_smtp_rcpt_to` in the provider db. // `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// Same as `DEFAULT_MAX_SMTP_RCPT_TO`, but for chatmail relays.
pub(crate) const DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO: usize = 999;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds). /// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours

View File

@@ -16,7 +16,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{ChatId, get_chat_cnt}; use crate::chat::{ChatId, get_chat_cnt};
use crate::config::Config; use crate::config::Config;
use crate::constants::{DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR}; use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR};
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging; use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events}; use crate::events::{Event, EventEmitter, EventType, Events};
@@ -587,6 +587,23 @@ impl Context {
self.get_config_bool(Config::IsChatmail).await self.get_config_bool(Config::IsChatmail).await
} }
/// Returns maximum number of recipients the provider allows to send a single email to.
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
let is_chatmail = self.is_chatmail().await?;
let val = self
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or_else(
|| match is_chatmail {
true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO,
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
},
usize::from,
);
Ok(val)
}
/// Does a single round of fetching from IMAP and returns. /// Does a single round of fetching from IMAP and returns.
/// ///
/// Can be used even if I/O is currently stopped. /// Can be used even if I/O is currently stopped.

View File

@@ -304,7 +304,7 @@ mod tests {
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
This message does not have Content-Type nor Subject.<br/> This message does not have Content-Type nor Subject.<br/>
</body></html> </body></html>
"# "#
@@ -322,7 +322,7 @@ This message does not have Content-Type nor Subject.<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/> message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
</body></html> </body></html>
"# "#
@@ -341,7 +341,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
This line ends with a space and will be merged with the next one due to format=flowed.<br/> This line ends with a space and will be merged with the next one due to format=flowed.<br/>
<br/> <br/>
This line does not end with a space<br/> This line does not end with a space<br/>
@@ -362,7 +362,7 @@ and will be wrapped as usual.<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
mime-modified should not be set set as there is no html and no special stuff;<br/> mime-modified should not be set set as there is no html and no special stuff;<br/>
although not being a delta-message.<br/> although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/> test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>

View File

@@ -1045,15 +1045,12 @@ impl Session {
if target.is_empty() { if target.is_empty() {
self.delete_message_batch(context, &uid_set, rowid_set) self.delete_message_batch(context, &uid_set, rowid_set)
.await .await
.with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?; .with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
} else { } else {
self.move_message_batch(context, &uid_set, rowid_set, &target) self.move_message_batch(context, &uid_set, rowid_set, &target)
.await .await
.with_context(|| { .with_context(|| {
format!( format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?; })?;
} }
} }
@@ -1287,9 +1284,10 @@ impl Session {
for (request_uids, set) in build_sequence_sets(&request_uids)? { for (request_uids, set) in build_sequence_sets(&request_uids)? {
info!(context, "Starting UID FETCH of message set \"{}\".", set); info!(context, "Starting UID FETCH of message set \"{}\".", set);
let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| { let mut fetch_responses = self
format!("fetching messages {} from folder \"{}\"", &set, folder) .uid_fetch(&set, BODY_FULL)
})?; .await
.with_context(|| format!("fetching messages {set} from folder {folder:?}"))?;
// Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here // Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
// when we want to process other messages first. // when we want to process other messages first.
@@ -1503,7 +1501,7 @@ impl Session {
.get_metadata( .get_metadata(
mailbox, mailbox,
options, options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients)", "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
) )
.await?; .await?;
for m in metadata { for m in metadata {
@@ -1539,21 +1537,6 @@ impl Session {
} }
} }
} }
"/shared/vendor/deltachat/maxsmtprecipients" => {
if let Some(value) = m.value.and_then(|v| v.parse::<u32>().ok()) {
let transport_id = self.transport_id();
context
.sql
.execute(
"UPDATE transports \
SET max_smtp_rcpt_to=? WHERE id=?",
(value, transport_id),
)
.await
.log_err(context)
.ok();
}
}
_ => {} _ => {}
} }
} }

View File

@@ -427,18 +427,16 @@ mod tests {
let self_chat = ctx1.get_self_chat().await; let self_chat = ctx1.get_self_chat().await;
let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap(); let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
assert_eq!(msgs.len(), 2); assert_eq!(msgs.len(), 2);
let msgid = match msgs.first().unwrap() { let ChatItem::Message { msg_id } = msgs.first().unwrap() else {
ChatItem::Message { msg_id } => msg_id, panic!("wrong chat item");
_ => panic!("wrong chat item"),
}; };
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap(); let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
let text = msg.get_text(); let text = msg.get_text();
assert_eq!(text, "hi there"); assert_eq!(text, "hi there");
let msgid = match msgs.get(1).unwrap() { let ChatItem::Message { msg_id } = msgs.get(1).unwrap() else {
ChatItem::Message { msg_id } => msg_id, panic!("wrong chat item");
_ => panic!("wrong chat item"),
}; };
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap(); let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
let path = msg.get_file(&ctx1).unwrap(); let path = msg.get_file(&ctx1).unwrap();
assert_eq!( assert_eq!(

View File

@@ -570,9 +570,11 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
pub struct Fingerprint(Vec<u8>); pub struct Fingerprint(Vec<u8>);
impl Fingerprint { impl Fingerprint {
/// Creates new 160-bit (20 bytes) fingerprint. /// Creates new fingerprint.
///
/// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys.
pub fn new(v: Vec<u8>) -> Fingerprint { pub fn new(v: Vec<u8>) -> Fingerprint {
debug_assert_eq!(v.len(), 20); debug_assert!(v.len() == 20 || v.len() == 32);
Fingerprint(v) Fingerprint(v)
} }
@@ -625,7 +627,10 @@ impl std::str::FromStr for Fingerprint {
.filter(|&c| c.is_ascii_hexdigit()) .filter(|&c| c.is_ascii_hexdigit())
.collect(); .collect();
let v: Vec<u8> = hex::decode(&hex_repr)?; let v: Vec<u8> = hex::decode(&hex_repr)?;
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}"); ensure!(
v.len() == 20 || v.len() == 32,
"wrong fingerprint length: {hex_repr}"
);
let fp = Fingerprint::new(v); let fp = Fingerprint::new(v);
Ok(fp) Ok(fp)
} }

View File

@@ -222,7 +222,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
} else { } else {
msg.timestamp_sort msg.timestamp_sort
}); });
ret += &format!("Received: {}", &s); ret += &format!("Received: {s}");
ret += "\n"; ret += "\n";
} }
@@ -301,7 +301,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
ret += "Type: "; ret += "Type: ";
ret += &format!("{}", msg.viewtype); ret += &format!("{}", msg.viewtype);
ret += "\n"; ret += "\n";
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default()); ret += &format!("Mimetype: {}\n", msg.get_filemime().unwrap_or_default());
} }
let w = msg.param.get_int(Param::Width).unwrap_or_default(); let w = msg.param.get_int(Param::Width).unwrap_or_default();
let h = msg.param.get_int(Param::Height).unwrap_or_default(); let h = msg.param.get_int(Param::Height).unwrap_or_default();

View File

@@ -1939,32 +1939,13 @@ pub(crate) fn render_outer_message(
/// Takes the encrypted part, wraps it in a MimePart, /// Takes the encrypted part, wraps it in a MimePart,
/// and sets the appropriate Content-Type for the outer message /// and sets the appropriate Content-Type for the outer message
pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> { pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
// XXX: additional newline is needed
// to pass filtermail at
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
let encrypted = encrypted + "\n";
MimePart::new( MimePart::new(
"multipart/encrypted; protocol=\"application/pgp-encrypted\"", "multipart/encrypted; protocol=\"application/pgp-encrypted\"",
vec![ vec![
// Autocrypt part 1 // Autocrypt part 1
MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header( MimePart::new("application/pgp-encrypted", "Version: 1\r\n"),
"Content-Description",
mail_builder::headers::raw::Raw::new("PGP/MIME version identification"),
),
// Autocrypt part 2 // Autocrypt part 2
MimePart::new( MimePart::new("application/octet-stream", encrypted),
"application/octet-stream; name=\"encrypted.asc\"",
encrypted,
)
.header(
"Content-Description",
mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"),
)
.header(
"Content-Disposition",
mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"),
),
], ],
) )
} }

View File

@@ -356,7 +356,7 @@ impl MimeMessage {
let decrypted_msg; // Decrypted signed OpenPGP message. let decrypted_msg; // Decrypted signed OpenPGP message.
let expected_sender_fingerprint: Option<String>; let expected_sender_fingerprint: Option<String>;
let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await { let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await {
Ok(Some((mut msg, expected_sender_fp))) => { Ok(Some((mut msg, expected_sender_fp))) => {
mail_raw = msg.as_data_vec().unwrap_or_default(); mail_raw = msg.as_data_vec().unwrap_or_default();

View File

@@ -109,8 +109,8 @@ pub(crate) async fn connect_tcp_inner(
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> { ) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr)) let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
.await .await
.context("Connection timeout")? .with_context(|| format!("Connection to {addr} timed out"))?
.context("Connection failure")?; .with_context(|| format!("Connection to {addr} failed"))?;
// Disable Nagle's algorithm. // Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?; tcp_stream.set_nodelay(true)?;
@@ -180,7 +180,7 @@ where
delay_set.spawn(tokio::time::sleep(delay)); delay_set.spawn(tokio::time::sleep(delay));
} }
let mut first_error = None; let mut all_errors = Vec::new();
let res = loop { let res = loop {
if let Some(fut) = futures.next() { if let Some(fut) = futures.next() {
@@ -200,7 +200,7 @@ where
} }
Ok(Err(err)) => { Ok(Err(err)) => {
// Some connection attempt failed. // Some connection attempt failed.
first_error.get_or_insert(err); all_errors.push(err);
} }
Err(err) => { Err(err) => {
break Err(err); break Err(err);
@@ -211,9 +211,11 @@ where
// Out of connection attempts. // Out of connection attempts.
// //
// Break out of the loop and return error. // Break out of the loop and return error.
break Err( break if all_errors.is_empty() {
first_error.unwrap_or_else(|| format_err!("No connection attempts were made")) Err(format_err!("No connection attempts were made"))
); } else {
Err(format_err!("All connection attempts failed: {}", all_errors.into_iter().map(|err| format!("{err:#}")).collect::<Vec<String>>().join("; ")))
};
} }
} }
}, },

View File

@@ -847,4 +847,41 @@ mod tests {
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err()); assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err()); assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
} }
/// Test PQC support.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pqc() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let pqc = &tcm.pqc().await;
let pqc_received_message = tcm.send_recv_accept(alice, pqc, "Hi!").await;
let pqc_chat_id = pqc_received_message.chat_id;
let pqc_sent = pqc.send_text(pqc_chat_id, "Hello back!").await;
let alice_rcvd = alice.recv_msg(&pqc_sent).await;
assert_eq!(alice_rcvd.text, "Hello back!");
Ok(())
}
/// Tests securejoin with inviter using PQC key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_securejoin_pqc_inviter() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let pqc = &tcm.pqc().await;
tcm.execute_securejoin(pqc, alice).await;
}
/// Tests securejoin with joiner using PQC key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_securejoin_pqc_joiner() {
let mut tcm = TestContextManager::new();
let pqc = &tcm.pqc().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, pqc).await;
}
} }

View File

@@ -39,7 +39,7 @@ impl PlainText {
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
"# "#
.to_string(); .to_string();
@@ -132,7 +132,7 @@ http://link-at-start-of-line.org
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
line 1<br/> line 1<br/>
line 2<br/> line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/> line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
@@ -156,7 +156,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
Foo<br/> Foo<br/>
bar<br/> bar<br/>
</body></html> </body></html>
@@ -178,7 +178,7 @@ bar<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>&gt; here!<br/> line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>&gt; here!<br/>
</body></html> </body></html>
"# "#
@@ -199,7 +199,7 @@ line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
line with nohttp://no.link here<br/> line with nohttp://no.link here<br/>
</body></html> </body></html>
"# "#
@@ -220,7 +220,7 @@ line with nohttp://no.link here<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/> just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/>
</body></html> </body></html>
"# "#
@@ -241,7 +241,7 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
line still line<br/> line still line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
<em>&gt;still quote</em><br/> <em>&gt;still quote</em><br/>
@@ -265,7 +265,7 @@ line still line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
linestill line<br/> linestill line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
<em>&gt;still quote</em><br/> <em>&gt;still quote</em><br/>
@@ -289,7 +289,7 @@ linestill line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
line <br/> line <br/>
still line<br/> still line<br/>
<em>&gt;quote </em><br/> <em>&gt;quote </em><br/>
@@ -314,7 +314,7 @@ still line<br/>
<html><head> <html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
</head><body> </head><body dir="auto" style="unicode-bidi: plaintext">
def foo():<br/> def foo():<br/>
&nbsp;&nbsp;&nbsp;&nbsp;pass<br/> &nbsp;&nbsp;&nbsp;&nbsp;pass<br/>
<br/> <br/>

View File

@@ -332,7 +332,7 @@ fn inner_generate_secure_join_qr_code(
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?; d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?; d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
d.attr("r", HALF_LOGO_SIZE)?; d.attr("r", HALF_LOGO_SIZE)?;
d.attr("style", format!("fill:{}", &color)) d.attr("style", format!("fill:{color}"))
})?; })?;
let avatar_font_size = LOGO_SIZE * 0.65; let avatar_font_size = LOGO_SIZE * 0.65;

View File

@@ -2344,8 +2344,6 @@ INSERT INTO msgs
/// Checks for "Chat-Edit" and "Chat-Delete" headers, /// Checks for "Chat-Edit" and "Chat-Delete" headers,
/// and edits/deletes existing messages accordingly. /// and edits/deletes existing messages accordingly.
///
/// Returns `true` if this message is an edit/deletion request.
async fn handle_edit_delete( async fn handle_edit_delete(
context: &Context, context: &Context,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
@@ -3560,12 +3558,7 @@ async fn create_or_lookup_mailinglist_or_broadcast(
mime_parser.timestamp_sent, mime_parser.timestamp_sent,
) )
.await .await
.with_context(|| { .with_context(|| format!("failed to create mailinglist '{name}' for grpid={listid}",))?;
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
if chattype == Chattype::InBroadcast { if chattype == Chattype::InBroadcast {
chat::add_to_chat_contacts_table( chat::add_to_chat_contacts_table(

View File

@@ -704,10 +704,10 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
assert_eq!(msg.state, MessageState::OutFailed); assert_eq!(msg.state, MessageState::OutFailed);
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?; let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
let ChatItem::Message { msg_id } = *msgs.last().unwrap() else { assert!(matches!(
panic!("Wrong item type"); *msgs.last().unwrap(),
}; ChatItem::Message { msg_id } if msg_id == msg.id
assert_eq!(msg_id, msg.id); ));
Ok(()) Ok(())
} }
@@ -1598,9 +1598,7 @@ async fn test_in_reply_to() {
// Load the first message from the same chat. // Load the first message from the same chat.
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await.unwrap(); let msgs = chat::get_chat_msgs(&t, msg.chat_id).await.unwrap();
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { let ChatItem::Message { msg_id } = msgs.first().unwrap() else {
msg_id
} else {
panic!("Wrong item type"); panic!("Wrong item type");
}; };

View File

@@ -218,7 +218,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStor
impl fmt::Debug for ConnectivityStore { impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() { if let Some(guard) = self.0.try_lock() {
write!(f, "ConnectivityStore {:?}", &*guard) write!(f, "ConnectivityStore {:?}", *guard)
} else { } else {
write!(f, "ConnectivityStore [LOCKED]") write!(f, "ConnectivityStore [LOCKED]")
} }

View File

@@ -73,8 +73,7 @@ fn shorten_name(name: &str, length: usize) -> String {
// We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers // We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers
format!( format!(
"{}_", "{}_",
&name name.chars()
.chars()
.take(length.saturating_sub(1)) .take(length.saturating_sub(1))
.collect::<String>() .collect::<String>()
) )

View File

@@ -992,7 +992,7 @@ async fn test_wrong_auth_token() -> Result<()> {
tcm.send_recv(alice, bob, "hi").await; tcm.send_recv(alice, bob, "hi").await;
let alice_qr = get_securejoin_qr(alice, None).await?; let alice_qr = get_securejoin_qr(alice, None).await?;
println!("{}", &alice_qr); println!("{alice_qr}");
let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey="); let invalid_alice_qr = alice_qr.replace("&s=", "&s=INVALIDAUTHTOKEN&someotherkey=");
join_securejoin(bob, &invalid_alice_qr).await?; join_securejoin(bob, &invalid_alice_qr).await?;

View File

@@ -2,8 +2,6 @@
mod connect; mod connect;
pub mod send; pub mod send;
#[cfg(test)]
mod chunking_tests;
use anyhow::{Context as _, Error, Result, bail, format_err}; use anyhow::{Context as _, Error, Result, bail, format_err};
use async_smtp::response::{Category, Code, Detail}; use async_smtp::response::{Category, Code, Detail};
@@ -12,7 +10,6 @@ use tokio::task;
use crate::chat::{ChatId, add_info_msg_with_cmd}; use crate::chat::{ChatId, add_info_msg_with_cmd};
use crate::config::Config; use crate::config::Config;
use crate::constants;
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::context::Context; use crate::context::Context;
use crate::events::EventType; use crate::events::EventType;
@@ -37,9 +34,6 @@ pub(crate) struct Smtp {
/// Email address we are sending from. /// Email address we are sending from.
from: Option<EmailAddress>, from: Option<EmailAddress>,
/// Transport used for the current connection.
transport_id: Option<u32>,
/// Timestamp of last successful send/receive network interaction /// Timestamp of last successful send/receive network interaction
/// (eg connect or send succeeded). On initialization and disconnect /// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None. /// it is set to None.
@@ -66,7 +60,6 @@ impl Smtp {
task::spawn(async move { transport.quit().await }); task::spawn(async move { transport.quit().await });
} }
self.last_success = None; self.last_success = None;
self.transport_id = None;
} }
/// Return true if smtp was connected but is not known to /// Return true if smtp was connected but is not known to
@@ -96,10 +89,9 @@ impl Smtp {
} }
self.connectivity.set_connecting(context); self.connectivity.set_connecting(context);
let (transport_id, lp) = ConfiguredLoginParam::load(context) let (_transport_id, lp) = ConfiguredLoginParam::load(context)
.await? .await?
.context("Not configured")?; .context("Not configured")?;
self.transport_id = Some(transport_id);
let proxy_config = ProxyConfig::load(context).await?; let proxy_config = ProxyConfig::load(context).await?;
self.connect( self.connect(
context, context,
@@ -173,7 +165,6 @@ impl Smtp {
} }
} }
#[derive(Debug)]
pub(crate) enum SendResult { pub(crate) enum SendResult {
/// Message was sent successfully. /// Message was sent successfully.
Success, Success,
@@ -185,36 +176,13 @@ pub(crate) enum SendResult {
Retry, Retry,
} }
pub(crate) trait SmtpSender: Send {
fn send_chunk<'a>(
&'a mut self,
context: &'a Context,
recipients: &'a [async_smtp::EmailAddress],
body: &'a str,
) -> futures::future::BoxFuture<'a, SendResult>;
}
struct RealSmtpSender<'a> {
smtp: &'a mut Smtp,
}
impl SmtpSender for RealSmtpSender<'_> {
fn send_chunk<'a>(
&'a mut self,
context: &'a Context,
recipients: &'a [async_smtp::EmailAddress],
body: &'a str,
) -> futures::future::BoxFuture<'a, SendResult> {
Box::pin(smtp_send(context, recipients, body, self.smtp))
}
}
/// Tries to send a message. /// Tries to send a message.
pub(crate) async fn smtp_send( pub(crate) async fn smtp_send(
context: &Context, context: &Context,
recipients: &[async_smtp::EmailAddress], recipients: &[async_smtp::EmailAddress],
message: &str, message: &str,
smtp: &mut Smtp, smtp: &mut Smtp,
msg_id: Option<MsgId>,
) -> SendResult { ) -> SendResult {
if recipients.is_empty() { if recipients.is_empty() {
return SendResult::Success; return SendResult::Success;
@@ -342,6 +310,25 @@ pub(crate) async fn smtp_send(
Ok(()) => SendResult::Success, Ok(()) => SendResult::Success,
}; };
if let SendResult::Failure(err) = &status
&& let Some(msg_id) = msg_id
{
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
}
}
}
status status
} }
@@ -419,40 +406,7 @@ pub(crate) async fn send_msg_to_smtp(
) )
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let transport_id = smtp let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await;
.transport_id
.context("SMTP not connected to a transport")?;
let chunk_size = max_smtp_rcpt_to(context, transport_id).await?;
let mut sender = RealSmtpSender { smtp };
let (status, start_idx) = send_smtp_chunks(
context,
&recipients_list,
body.as_str(),
chunk_size,
&mut sender,
)
.await;
let unsent_saved = start_idx < recipients_list.len();
if let Some(unsent) = recipients_list.get(start_idx..)
&& !unsent.is_empty()
{
let unsent_str: String = unsent
.iter()
.map(|a| a.as_ref())
.collect::<Vec<&str>>()
.join(" ");
context
.sql
.execute(
"UPDATE smtp SET recipients=? WHERE id=?",
(unsent_str, rowid),
)
.await
.log_err(context)
.ok();
}
match status { match status {
SendResult::Retry => {} SendResult::Retry => {}
@@ -501,16 +455,11 @@ pub(crate) async fn send_msg_to_smtp(
.await?; .await?;
}; };
} }
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, &err.to_string()).await?;
}
if !unsent_saved {
context context
.sql .sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,)) .execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await?; .await?;
} }
}
}; };
match status { match status {
@@ -521,38 +470,9 @@ pub(crate) async fn send_msg_to_smtp(
} }
Ok(()) Ok(())
} }
SendResult::Failure(err) => { SendResult::Failure(err) => Err(format_err!("{err}")),
if unsent_saved {
Err(format_err!("Retry"))
} else {
Err(format_err!("{err}"))
} }
} }
}
}
async fn max_smtp_rcpt_to(context: &Context, transport_id: u32) -> Result<usize> {
let limit = context
.sql
.query_row_optional(
"SELECT max_smtp_rcpt_to FROM transports WHERE id=?",
(transport_id,),
|row| row.get::<_, u32>(0),
)
.await?
.unwrap_or(0);
if limit > 0 {
return Ok(limit as usize);
}
let val = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
Ok(val)
}
pub(crate) async fn msg_has_pending_smtp_job( pub(crate) async fn msg_has_pending_smtp_job(
context: &Context, context: &Context,
@@ -680,7 +600,7 @@ async fn send_mdn_rfc724_mid(
}) })
.collect(); .collect();
match smtp_send(context, &recipients, &body, smtp).await { match smtp_send(context, &recipients, &body, smtp, None).await {
SendResult::Success => { SendResult::Success => {
if !recipients.is_empty() { if !recipients.is_empty() {
info!(context, "Successfully sent MDN for {rfc724_mid}."); info!(context, "Successfully sent MDN for {rfc724_mid}.");
@@ -802,22 +722,3 @@ pub(crate) async fn add_self_recipients(
} }
Ok(()) Ok(())
} }
#[allow(clippy::arithmetic_side_effects)]
pub(crate) async fn send_smtp_chunks(
context: &Context,
recipients: &[async_smtp::EmailAddress],
body: &str,
chunk_size: usize,
sender: &mut (dyn SmtpSender + Send),
) -> (SendResult, usize) {
for (i, chunk) in recipients.chunks(chunk_size).enumerate() {
let status = sender.send_chunk(context, chunk, body).await;
match status {
SendResult::Success => continue,
SendResult::Failure(_) => return (status, (i + 1) * chunk_size),
SendResult::Retry => return (status, i * chunk_size),
}
}
(SendResult::Success, recipients.len())
}

View File

@@ -1,102 +0,0 @@
use crate::smtp::{send_smtp_chunks, SendResult, SmtpSender};
use crate::test_utils::TestContextManager;
use crate::context::Context;
use anyhow::Result;
use futures::future::{BoxFuture, FutureExt};
/// Result the mock should return on the designated call.
enum MockFailure {
Transient,
Permanent,
}
struct MockSmtpSender {
call_count: usize,
fail_on_call: Option<(usize, MockFailure)>,
}
impl SmtpSender for MockSmtpSender {
fn send_chunk<'a>(
&'a mut self,
_context: &'a Context,
_recipients: &'a [async_smtp::EmailAddress],
_body: &'a str,
) -> BoxFuture<'a, SendResult> {
self.call_count += 1;
let count = self.call_count;
let fail_on = self.fail_on_call.as_ref().map(|(n, _)| *n);
let is_permanent = matches!(
self.fail_on_call,
Some((_, MockFailure::Permanent))
);
async move {
if fail_on == Some(count) {
if is_permanent {
return SendResult::Failure(
anyhow::format_err!("permanent error"),
);
}
return SendResult::Retry;
}
SendResult::Success
}
.boxed()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_smtp_chunks() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let recipients: Vec<_> = ["r1@ex.org", "r2@ex.org", "r3@ex.org", "r4@ex.org", "r5@ex.org"]
.iter()
.map(|a| async_smtp::EmailAddress::new(a.to_string()).unwrap())
.collect();
// All chunks succeed.
let mut sender = MockSmtpSender { call_count: 0, fail_on_call: None };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Success));
assert_eq!(processed, 5);
assert_eq!(sender.call_count, 3); // chunks: [2, 2, 1]
// Second chunk gets a transient error, only first chunk's recipients are processed.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Transient)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Retry));
assert_eq!(processed, 2);
assert_eq!(sender.call_count, 2);
// Last chunk gets a transient error, first two chunks' recipients are processed.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Transient)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Retry));
assert_eq!(processed, 4);
assert_eq!(sender.call_count, 3);
// Second chunk gets a permanent error; processed includes the failed chunk.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((2, MockFailure::Permanent)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Failure(_)));
assert_eq!(processed, 4);
assert_eq!(sender.call_count, 2);
// Last chunk gets a permanent error; processed includes the failed chunk.
let mut sender =
MockSmtpSender { call_count: 0, fail_on_call: Some((3, MockFailure::Permanent)) };
let (status, processed) =
send_smtp_chunks(&alice.ctx, &recipients, "body", 2, &mut sender).await;
assert!(matches!(status, SendResult::Failure(_)));
assert_eq!(processed, 6); // capped at (i+1)*chunk_size, may exceed len
assert_eq!(sender.call_count, 3);
Ok(())
}

View File

@@ -2378,22 +2378,13 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
sql.execute_migration( sql.execute_migration(
" "
UPDATE msgs SET state=26 WHERE state=28; -- Change OutMdnRcvd to OutDelivered. UPDATE msgs SET state=26 WHERE state=28; -- Change OutMdnRcvd to OutDelivered.
UPDATE msgs SET state=19 WHERE state=24; -- Change OutPreparing to OutFailed. UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
", ",
migration_version, migration_version,
) )
.await?; .await?;
} }
inc_and_check(&mut migration_version, 153)?;
if dbversion < migration_version {
sql.execute_migration(
"ALTER TABLE transports ADD COLUMN max_smtp_rcpt_to INTEGER NOT NULL DEFAULT 0",
migration_version,
)
.await?;
}
let new_version = sql let new_version = sql
.get_raw_config_int(VERSION_CFG) .get_raw_config_int(VERSION_CFG)
.await? .await?

View File

@@ -137,6 +137,17 @@ impl TestContextManager {
.await .await
} }
/// Returns a new "device" with a preconfigured v6 PQC key.
pub async fn pqc(&mut self) -> TestContext {
TestContext::builder()
.with_key_pair(pqc_keypair())
.with_address("pqc@example.org".to_string())
.with_id_offset(7000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
}
/// Creates a new unconfigured test account. /// Creates a new unconfigured test account.
pub async fn unconfigured(&mut self) -> TestContext { pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder() TestContext::builder()
@@ -304,6 +315,9 @@ impl TestContextManager {
pub struct TestContextBuilder { pub struct TestContextBuilder {
key_pair: Option<SignedSecretKey>, key_pair: Option<SignedSecretKey>,
/// Email address.
address: Option<String>,
/// Log sink if set. /// Log sink if set.
/// ///
/// If log sink is not set, /// If log sink is not set,
@@ -328,6 +342,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(alice_keypair())`. /// This is a shortcut for `.with_key_pair(alice_keypair())`.
pub fn configure_alice(self) -> Self { pub fn configure_alice(self) -> Self {
self.with_key_pair(alice_keypair()) self.with_key_pair(alice_keypair())
.with_address("alice@example.org".to_string())
} }
/// Configures as bob@example.net with fixed secret key. /// Configures as bob@example.net with fixed secret key.
@@ -335,6 +350,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(bob_keypair())`. /// This is a shortcut for `.with_key_pair(bob_keypair())`.
pub fn configure_bob(self) -> Self { pub fn configure_bob(self) -> Self {
self.with_key_pair(bob_keypair()) self.with_key_pair(bob_keypair())
.with_address("bob@example.net".to_string())
} }
/// Configures as charlie@example.net with fixed secret key. /// Configures as charlie@example.net with fixed secret key.
@@ -342,6 +358,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(charlie_keypair())`. /// This is a shortcut for `.with_key_pair(charlie_keypair())`.
pub fn configure_charlie(self) -> Self { pub fn configure_charlie(self) -> Self {
self.with_key_pair(charlie_keypair()) self.with_key_pair(charlie_keypair())
.with_address("charlie@example.net".to_string())
} }
/// Configures as dom@example.net with fixed secret key. /// Configures as dom@example.net with fixed secret key.
@@ -349,6 +366,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(dom_keypair())`. /// This is a shortcut for `.with_key_pair(dom_keypair())`.
pub fn configure_dom(self) -> Self { pub fn configure_dom(self) -> Self {
self.with_key_pair(dom_keypair()) self.with_key_pair(dom_keypair())
.with_address("dom@example.net".to_string())
} }
/// Configures as elena@example.net with fixed secret key. /// Configures as elena@example.net with fixed secret key.
@@ -356,6 +374,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(elena_keypair())`. /// This is a shortcut for `.with_key_pair(elena_keypair())`.
pub fn configure_elena(self) -> Self { pub fn configure_elena(self) -> Self {
self.with_key_pair(elena_keypair()) self.with_key_pair(elena_keypair())
.with_address("elena@example.net".to_string())
} }
/// Configures as fiona@example.net with fixed secret key. /// Configures as fiona@example.net with fixed secret key.
@@ -363,6 +382,7 @@ impl TestContextBuilder {
/// This is a shortcut for `.with_key_pair(fiona_keypair())`. /// This is a shortcut for `.with_key_pair(fiona_keypair())`.
pub fn configure_fiona(self) -> Self { pub fn configure_fiona(self) -> Self {
self.with_key_pair(fiona_keypair()) self.with_key_pair(fiona_keypair())
.with_address("fiona@example.net".to_string())
} }
/// Configures the new [`TestContext`] with the provided [`SignedSecretKey`]. /// Configures the new [`TestContext`] with the provided [`SignedSecretKey`].
@@ -374,6 +394,12 @@ impl TestContextBuilder {
self self
} }
/// Sets email address.
pub fn with_address(mut self, address: String) -> Self {
self.address = Some(address);
self
}
/// Attaches a [`LogSink`] to this [`TestContext`]. /// Attaches a [`LogSink`] to this [`TestContext`].
/// ///
/// This is useful when using multiple [`TestContext`] instances in one test: it allows /// This is useful when using multiple [`TestContext`] instances in one test: it allows
@@ -396,16 +422,7 @@ impl TestContextBuilder {
/// Builds the [`TestContext`]. /// Builds the [`TestContext`].
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext { pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
if let Some(key_pair) = self.key_pair { if let Some(key_pair) = self.key_pair {
let userid = { let addr = self.address.expect("Address is not set").clone();
let public_key = key_pair.to_public_key();
let id_bstr = public_key.details.users.first().unwrap().id.id();
String::from_utf8(id_bstr.to_vec()).unwrap()
};
let addr = mailparse::addrparse(&userid)
.unwrap()
.extract_single_info()
.unwrap()
.addr;
let name = EmailAddress::new(&addr).unwrap().local; let name = EmailAddress::new(&addr).unwrap().local;
let mut unused_name = name.clone(); let mut unused_name = name.clone();
@@ -1420,6 +1437,13 @@ pub fn fiona_keypair() -> SignedSecretKey {
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap() key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap()
} }
/// Loads a pre-generated v6 PQC keypair from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub fn pqc_keypair() -> SignedSecretKey {
key::SignedSecretKey::from_asc(include_str!("../test-data/key/pqc-secret.asc")).unwrap()
}
/// Utility to help wait for and retrieve events. /// Utility to help wait for and retrieve events.
/// ///
/// This buffers the events in order they are emitted. This allows consuming events in /// This buffers the events in order they are emitted. This allows consuming events in
@@ -1557,9 +1581,7 @@ pub(crate) async fn get_chat_msg(
asserted_msgs_count, asserted_msgs_count,
msgs.len() msgs.len()
); );
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] { let ChatItem::Message { msg_id } = msgs[index] else {
msg_id
} else {
panic!("Wrong item type"); panic!("Wrong item type");
}; };
Message::load_from_db(&t.ctx, msg_id).await.unwrap() Message::load_from_db(&t.ctx, msg_id).await.unwrap()
@@ -1685,7 +1707,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
msg.get_id(), msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" }, if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" }, if msg.has_location() { "📍" } else { "" },
&contact_name, contact_name,
contact_id, contact_id,
msgtext, msgtext,
if msg.get_from_id() == ContactId::SELF { if msg.get_from_id() == ContactId::SELF {

View File

@@ -161,7 +161,7 @@ async fn check_that_transition_worked(
2, 2,
"Group {} has members {:?}, but should have members {:?} and {:?}", "Group {} has members {:?}, but should have members {:?} and {:?}",
group, group,
&members, members,
alice_contact_id, alice_contact_id,
ContactId::SELF ContactId::SELF
); );

View File

@@ -62,13 +62,13 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<'_, str> {
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) { if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
Cow::Owned(format!( Cow::Owned(format!(
"{}{}", "{}{}",
&buf.get(..=index).unwrap_or_default(), buf.get(..=index).unwrap_or_default(),
DC_ELLIPSIS DC_ELLIPSIS
)) ))
} else { } else {
Cow::Owned(format!( Cow::Owned(format!(
"{}{}", "{}{}",
&buf.get(..end_pos).unwrap_or_default(), buf.get(..end_pos).unwrap_or_default(),
DC_ELLIPSIS DC_ELLIPSIS
)) ))
} }

View File

@@ -247,12 +247,12 @@ proptest! {
assert!( assert!(
l <= approx_chars + el_len, l <= approx_chars + el_len,
"buf: '{}' - res: '{}' - len {}, approx {}", "buf: '{}' - res: '{}' - len {}, approx {}",
&buf, &res, res.len(), approx_chars buf, res, res.len(), approx_chars
); );
if buf.chars().count() > approx_chars + el_len { if buf.chars().count() > approx_chars + el_len {
let l = res.len(); let l = res.len();
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res); assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {res}");
} }
} }
} }

View File

@@ -116,7 +116,7 @@ pub(crate) struct ConnectionCandidate {
impl fmt::Display for ConnectionCandidate { impl fmt::Display for ConnectionCandidate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", &self.host, self.port, self.security)?; write!(f, "{}:{}:{}", self.host, self.port, self.security)?;
Ok(()) Ok(())
} }
} }
@@ -131,7 +131,7 @@ pub(crate) struct ConfiguredServerLoginParam {
impl fmt::Display for ConfiguredServerLoginParam { impl fmt::Display for ConfiguredServerLoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.connection, &self.user)?; write!(f, "{}:{}", self.connection, self.user)?;
Ok(()) Ok(())
} }
} }

View File

@@ -0,0 +1,39 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
xUsGaf8NSRsAAAAgYy+GaofURMeV0+bcZZGY2ZdAamU+LG69ONjd3haVU3cAhm6G
IT/UEgFgVdPEhiXER9cfPLiCgkiw/L5mrAZfuLfCqgYfGwgAAABLBQJp/w1JIiEG
hys0q6D+DFWPnwQoWtuX0mL6ovH2kCjWmDufAFmB0+QCGwMCHgkECwkIBwYVCg4J
CAwBFg0nCQIIAgcCCQEIAQcBAAAAAEGQEO9Py9Q7njj1WXhtn1wMJSLBdHBE+qQu
RaCaiWkY5l4EWLlVRPAjX2bBSGq6n3+M+H6oFpOHETAX8IcFSxc260UD+PM0jQpV
H6ReNy7PBCQKx8RrBmn/DUkjAAAEwPmkVcPy1ye0/7D9nDQCkENUGry97iLkpcw/
tLJfzL5gJAdzrPkDkyukHxrO7kiUx+mzpiGZRZeyRgBd5YQ+mTgGrptxXLFHcKFR
79Fjg1UjgHEFjxCkCHUfnNcGZVM3p5skESnNgzsgFGiODfKhM4ew3AFgkUc5LNZj
Zgpgt4ETIhylbLUY89ccfNpKnQeJl3cv8lvA/yqhoUutJXwZQ/qYKFnEIGEBTFto
hLZn0KauF9KYOYvOV4yjeZQBlxSPNAWj9SqSNcalpTUFzwoQVSsqWwiys1PEzGAu
twQVKsZ3e/hlZAyR4eGMiYEmCEy7qjuaOJsqHQuW7hdOHWdVRUpRHOtfj3QAzdc0
CehVbyCRJVwnTSKiT3AYsdACH8U7mhI5/VxeSHNRIDN1Y6g6N5sx6Wur/HuKGFwx
L4urdPdpJJgyLXR8GUkL/yeqUhogu4mbVAmULbq2BCIKFNpMyGdhnDugN6Sp5MWc
GOxCW7CASuBYPHW/rto0C4M/3gCtN2sPtRAhOsXNBBhMqLlzzCgawulCiGtNjHUK
HsVhghgYwKRBT7vLSKDNsCVizzoZxNQq8yUEXpFIRsTGt3wYoigZn4wOSpmQbxGe
P3Uc2GWuuukCBNEP5oW4+TCFaNw5mvZgZwl5n4K34poxVgpqBIM2m2fEu8oyLPJZ
bBxnbty3MUAdLpxv+0otGSHJF4xa3lsEyUdr6+JZZXohNXKoyjeJMGo6qPkvCADI
upMnDSYZeLU5bVstHWS6otuRMEcjdLBkYfqfzBhkzbptscaUXzsaK4cd/iQzAA1r
A0ygvcA78Vo363cElNAJh3lntrZZGpBYnzcU/zLACKAVJCYPy3Cj8Al8x+gHP0Yr
ZSOYdZA1q9s2Kuqk7upCpcYDZ+uXGZs3ubA0TYCcO3FKhAwLhzJad5WApBFETYt2
3KJEwgEjQaCs7sNNiwaKxhLC2VJhUckgluGs5iUu9ck5jdU+N9MqTmloF/u2Gok8
QEqF9+DBhPg/fJoI9sN8sIyLrksEUQsm59mvJbVWOpxtbwpWZ+J4cat4azHE0khy
rolL6lZqDJYW4xVeoAVl5iccicjE6mJLemoxf6iJdohi5cN5JXyZtgtdsbIesJib
BLPJVahmv5W1Q4RmrwEp5Ua4xra5Mcac4PeINTOkGMErIhdvnuxEH/Cxd8VKhNlU
vdty4MOyUOkRRPhOMNKUyTkwS9yjprK3QbhEJgrJygHCGpQ0jwp9PrtKqNnOONSX
t4ZORuiAYHDFz3DPlJhLLzNoAJse0RAyolkPThoMl2JlY5ci8pVHb+Ed/kaeFxnE
UJJIOZvDFfNCFCM5CCXG/2pi/icA7nHPDFVBeYPMz1B5vrgmdDNMMFVQNMBtrroT
4pi1N5U4+EAv1vah40akQ/iFcfZjt4sE/jG0M0NCWqHBDPO7e0ae+2IqnIJmsHjL
N1ak3egY00CnRHdrPCkOkhooFIHA1hYxIwyP07qfkhUBNwSqZ4AfF8UW/nuLjeaj
ajEWz3zGLvQpfHSobEGPQKk+eIA1fOVeAuJAUAmJz5YO1Dk4OfczeQqQiOhtv+qe
PYaZQfBFJVamGocDHomPQkP/IvAJhuO9xWPapqbdRwGfVRJZgGsAy89mT1w0PU1C
u6VpIoyZB2J9LZkw9qb9sRRJAr2gpWGBD4CCmPZ8d17ZGDcIr8o+eI+bo5eKf+1j
6NhsjM7AmIccStNxZYWE4ZucvYYbPvT3ns/TNa7BH2DBqfGK84PawosGGBsIAAAA
LAUCaf8NSQIbDCIhBocrNKug/gxVj58EKFrbl9Ji+qLx9pAo1pg7nwBZgdPkAAAA
ADrcEIqnwTwJoiZAxzK+w7uQFHzsYMWIj8x+DKsn7D1silKINHDnFSrlSKRtbAW6
x9+HrN/nvR7bOnXZvZhz7lQ3Lp3YUdzEcqRMj8BWW8IXdm0C
-----END PGP PRIVATE KEY BLOCK-----